English
Tools
The SDK exposes defineTool() as its only custom-tool authoring API. It uses a TypeBox parameter schema and returns a ToolDefinition accepted directly by Agent and Session.
Internally, every tool executes as AsyncGenerator<ToolYield, ToolResult>. defineTool() accepts one public execution contract: an async function that returns JSON data. The SDK wraps that data as an internal successful result.
defineTool
ts
import { defineTool } from '@blade-ai/agent-sdk';
import Type from 'typebox';
const searchDocs = defineTool({
name: 'SearchDocs',
description: 'Search the documentation index',
parameters: Type.Object({
query: Type.String(),
limit: Type.Optional(Type.Number()),
}),
async execute(params) {
const results = await search(params.query, params.limit ?? 10);
return { results, count: results.length };
},
});The returned JSON value becomes both model and data on the internal success result. Throw an error to report failure. Public definitions do not return ToolResult or async generators, so result meaning never depends on object shape or iterator methods.
The TypeBox schema is the single source of truth for parameter inference, runtime validation, and the model-facing JSON Schema. execute parameters are inferred through Type.Static<TSchema>. Heterogeneous definition erasure happens only inside Session; application code does not declare that internal type.
What you can omit
Only name, description, parameters, and execute are required:
sideEffectis optional. When it is omitted the tool counts asnon_idempotent, so recovery never replays it. DeclareToolSideEffect.PUREorToolSideEffect.IDEMPOTENTon read-only or safely repeatable tools to admit them into retryable recovery.parametersaccepts a TypeBox schema. The same schema object is sent to the model and compiled by the SDK for runtime validation. There is no Zod/JSON Schema conversion or advisory-only path.
ts
import { defineTool } from '@blade-ai/agent-sdk';
import Type from 'typebox';
const lookup = defineTool({
name: 'Lookup',
description: 'Look up one record by id',
parameters: Type.Object({ id: Type.String() }),
async execute({ id }) {
return await lookupRecord(id);
},
});kind and sideEffect may both be omitted. When you do set kind, use the ToolKind constants (ToolKind.ReadOnly, ToolKind.Write, or ToolKind.Execute) imported from @blade-ai/agent-sdk; TypeScript does not accept raw string literals for it.
Capabilities on demand
Use services to declare Session services required by a tool. Its execute context exposes only declared services. Set requiresRuntime: true to request execution lease and fencing capabilities:
ts
const delegated = defineTool({
name: 'Delegate',
description: 'Delegate a task',
parameters: Type.Object({ prompt: Type.String() }),
services: ['subagentRegistry'],
requiresRuntime: true,
async execute({ prompt }, context) {
await context.runtime.assertExecutionLease();
return { prompt, agents: context.subagentRegistry.getAllNames() };
},
});ToolServiceName defines the available service names. If a Session lacks any declared service, it does not register the tool. Undeclared services are not exposed, and ordinary tools receive no runtime property.
Streaming contract
ts
type ToolYield =
| {
kind: 'progress';
message?: string;
data?: JsonValue;
completed?: number;
total?: number;
resumeToken?: string;
}
| {
kind: 'message';
content: ToolDisplayContent;
}
| {
kind: 'effect';
effect: ToolEffect;
};
type ToolExecution<TData extends JsonValue = JsonValue> =
AsyncGenerator<ToolYield, ToolResult<TData>, void>;ToolExecution is the SDK's internal runtime protocol. Public defineTool() callbacks return Promise<JsonValue> and are compiled into that protocol by the Registry; the SDK does not infer result kinds from fields or iterator methods.
ToolResult
ts
type ToolResult =
| {
status: 'success';
model: ToolModelContent;
display?: ToolDisplayContent;
data?: JsonValue;
metadata?: ToolResultMetadata;
}
| {
status: 'error';
model: ToolModelContent;
display?: ToolDisplayContent;
error: ToolError;
metadata?: ToolResultMetadata;
};modelis written back to model context.displayis UI-facing content and should not be parsed to reconstruct model output.datais optional caller-facing structured data and must be a strict JSON value. Large-result artifact persistence applies tomodel, notdata.- Failed results require both
status: 'error'anderror.
Runtime progress, messages, and effects
The compiled runtime Tool protocol can yield events in the order they happen. This protocol is used by SDK-owned tools and middleware; it is not a second defineTool() return shape:
ts
async *execute(params) {
yield {
kind: 'progress',
message: 'Uploading',
completed: 1,
total: 3,
};
yield {
kind: 'message',
content: { summary: 'Upload started' },
};
yield {
kind: 'effect',
effect: {
type: 'contextPatch',
patch: { metadata: { uploadId: 'upload-1' } },
},
};
return {
status: 'success',
model: { uploadId: 'upload-1' },
};
}Effects can update runtime policy, context, messages, or permissions. The Session stream projects them into corresponding tool_* events.
Community tool package convention
Reusable third-party tool packages use the blade-tool-* naming convention:
- Unscoped package:
blade-tool-github - Scoped package:
@acme/blade-tool-jira - Names must use lowercase kebab-case and identify the capability or target system rather than a generic label.
The package root should export a named factory such as createGithubTool, or a stable tools array. Do not import SDK src/, generated dist/ chunks, or other private paths. Tool packages may depend only on public definitions, types, and constants from the root SDK entrypoint.
json
{
"name": "@acme/blade-tool-jira",
"peerDependencies": {
"@blade-ai/agent-sdk": "^7.4.0"
}
}Before publishing, a tool package must:
- Declare accurate
sideEffectandinterruptBehaviorvalues for every tool. - Use TypeBox parameter schemas and return serializable data.
- Accept credentials from the caller instead of reading or embedding implicit global credentials.
- Validate protocols, redirects, and private addresses for network access, and honor
ExecutionContextfilesystem capabilities for file access. - Never retry non-idempotent effects automatically, and release processes, connections, and temporary resources after cancellation.
- Document tool names, permissions, environment variables, side effects, and a minimal usage example in its README.
Interruption
interruptBehavior controls a tool when a priority: 'now' input arrives. Public defineTool() definitions use the conservative block default:
blockis the default. The tool completes before steering is applied.cancelis for tools that observecontext.signaland reliably release resources.
Explicit session.abort() and session.close() are request-level cancellation and are not blocked by interruptBehavior: 'block'. Both methods wait for active tool cleanup, so custom tools must honor the request AbortSignal even when they block now-priority steering.
SessionOptions.toolTimeoutMs bounds each tool invocation and defaults to 600000 (10 minutes). The deadline starts after permission checks and the durable tool_started boundary, remains active across progress yields, and aborts the tool's signal on expiry. The terminal result has ToolErrorType.TIMEOUT_ERROR. Cleanup is awaited for at most 5 seconds. If it is still pending, the pipeline refuses new tool work and Session shutdown or handoff fails closed until the generator exits; JavaScript cannot preempt custom tool code that ignores cancellation.
Permission waits are cancellation-bounded instead of time-bounded because a human approval may legitimately remain open. Input validation and tool-level permission checks receive ExecutionContext.signal; AgentOptions.advanced.permission and the low-level permissionHandler receive request.signal; interactive handlers receive ConfirmationDetails.abortSignal. The pipeline races every callback against that request signal. A callback should stop work when it aborts. If it ignores the signal, the request still cancels, but new tool calls and Session close/handoff fail closed until the callback Promise settles. A durable permission request is resolved with decision: 'cancel' before cancellation completes.
Concurrency-slot and same-file lock waits also occur before the tool timeout starts, but both observe the active Request signal. Cancellation removes a queued waiter without consuming capacity or disturbing FIFO order. If resource grant and cancellation happen in the same turn, the pipeline rechecks the signal and releases every acquired lease before returning the cancellation result.
defineTool() / ToolDefinition does not expose interruptBehavior. Custom tools must still observe context.signal and clean up promptly during explicit session.abort() or session.close().
Side-effect contract
Every tool should declare an accurate sideEffect. A ToolDefinition that omits it is treated conservatively as non_idempotent:
pure: does not mutate external state and can be replayed during recovery.idempotent: repeating the same invocation reaches the same intended state and can be replayed during recovery.non_idempotent: repeating an invocation may create additional effects, so a started call requires operator or external-system reconciliation.
ToolKind, isReadOnly, and sideEffect are independent dimensions; the SDK does not infer one from another. Parameter-dependent tools may narrow the contract through resolveBehavior(), but their static declaration must be the most conservative value. Dynamic MCP tools always use non_idempotent; remote annotations are hints and are not sufficient evidence for safe automatic replay.
ToolDefinition
ts
interface ToolDefinition<
TSchema extends Type.TSchema = Type.TSchema,
TData extends JsonValue = JsonValue,
> {
name: string;
aliases?: string[];
displayName?: string;
description: string | ToolDescription;
parameters: TSchema;
sideEffect?: ToolSideEffect;
kind?: ToolKind;
group?: BuiltinToolGroup;
exposure?: ToolExposureConfig;
services?: readonly ToolServiceName[];
requiresRuntime?: boolean;
execute(
params: Type.Static<TSchema>,
context: ExecutionContext,
): ToolExecution<TData>;
}ExecutionContext
ts
interface ExecutionContext {
sessionId?: SessionId;
messageId?: MessageId;
contextSnapshot?: ContextSnapshot;
signal?: AbortSignal;
confirmationHandler?: ConfirmationHandler;
permissionMode?: PermissionMode;
bladeConfig?: BladeConfig;
}Session services named in services are added to the authoring context on demand. Setting requiresRuntime: true also provides readonly context.runtime; registry, exposure-planning, and durable lifecycle capabilities are not handed directly to tools.
ts
interface ConfirmationDetails {
// ...
abortSignal?: AbortSignal;
}
interface ConfirmationHandler {
requestConfirmation(
details: ConfirmationDetails,
): Promise<ConfirmationResponse>;
}Durable lifecycle boundaries
A runtime can use ToolExecutionLifecycle to observe and block critical tool persistence boundaries:
ts
interface ToolExecutionLifecycle {
onToolScheduled?(
event: ToolScheduledLifecycle,
): Promise<ToolInvocationLifecycle | undefined>;
onToolSettled?(event: ToolSettledLifecycle): Promise<void>;
}
interface ToolScheduledLifecycle {
toolCallId: ToolUseId;
toolName: string;
modelAttemptId?: ModelAttemptId;
modelInput: JsonObject; // Original provider arguments.
input: JsonObject;
sideEffect: ToolSideEffect;
interruptBehavior: 'block' | 'cancel';
}
interface ToolExecutionStartedLifecycle {
input: JsonObject;
sideEffect: ToolSideEffect;
}
interface ToolInvocationLifecycle {
onPermissionRequested?(
details: ConfirmationDetails,
input: JsonObject,
): Promise<PermissionRequestId>;
onPermissionResolved?(
resolution: ToolPermissionResolution,
): Promise<void>;
onExecutionStarted?(
event: ToolExecutionStartedLifecycle,
): Promise<void>;
}These callbacks are not best-effort telemetry. Their ordering is fixed:
onToolScheduledcompletes beforetool_startis published.onPermissionRequestedcompletes before the interactive confirmation handler runs.onPermissionResolvedcompletes before the permission decision is accepted.onExecutionStartedpersists the final post-permission input and resolved side-effect contract before invoking the tool generator, so a durable write failure blocks the side effect.onToolSettledcompletes beforetool_resultis published.
Invalid JSON arguments and synthetic interruption results for calls that were never dispatched do not enter the durable lifecycle because they never form an executable invocation. Without a lifecycle observer, normal tool execution behavior is unchanged.
Built-in tools
getBuiltinTools() is exported by /advanced. Its local Session facade registers this local host tool set automatically. The returned list contains all static built-in candidates; the Session registry skips tools whose declared services are unavailable. MemoryRead and MemoryWrite are registered only when SessionOptions.memoryManager is configured. Calling a returned tool's execute() method directly bypasses the ExecutionPipeline. Existing-file Write and Edit calls still require ExecutionContext.sessionId; without it, read-before-write cannot be verified and the operation fails closed.
| Group | Tools |
|---|---|
| Filesystem | Read, Edit, Write, NotebookEdit, Glob, Grep |
| Shell | Bash, KillShell |
| Web | WebFetch, WebSearch |
| Subagents | Task, TaskOutput |
| Structured tasks | TaskCreate, TaskGet, TaskUpdate, TaskList, TaskStop |
| System | AskUserQuestion, DiscoverTools, Skill |
| Planning | EnterPlanMode, ExitPlanMode |
| Todos | TodoWrite |
| Memory | MemoryRead, MemoryWrite (requires SessionOptions.memoryManager) |
| MCP resources | ListMcpResources, ReadMcpResource |
Built-in implementations share four narrow capability owners. file/operationCore.ts owns authorized paths, write guards, and file-operation failures; search/searchRunner.ts owns search paths and execution; web/webRequest.ts owns timeouts, cancellation, proxies, redirects, providers, and caching; task/taskCrud.ts declares all structured task CRUD tools over one TaskStore.
Built-in contracts:
| Tool | Kind | Side effect |
|---|---|---|
Read | ReadOnly | pure |
Edit | Write | non_idempotent |
Write | Write | idempotent |
NotebookEdit | Write | non_idempotent; replace narrows to idempotent |
Glob, Grep | ReadOnly | pure |
Bash | Execute | non_idempotent; read-only foreground commands narrow to pure |
KillShell | Execute | idempotent |
WebFetch | Execute | non_idempotent; GET/HEAD narrow to pure, PUT/DELETE to idempotent |
WebSearch | ReadOnly | pure |
Task | ReadOnly | non_idempotent |
TaskOutput | ReadOnly | non_idempotent |
TaskCreate | Write | non_idempotent |
TaskGet, TaskList | Write | pure |
TaskUpdate, TaskStop | Write | idempotent |
TodoWrite | ReadOnly | idempotent |
MemoryRead | ReadOnly | pure |
MemoryWrite | Write | idempotent |
EnterPlanMode, ExitPlanMode, AskUserQuestion | ReadOnly | non_idempotent |
DiscoverTools | ReadOnly | idempotent |
Skill | Execute | non_idempotent |
ListMcpResources, ReadMcpResource | ReadOnly | pure |
Permission decisions use the resolved behavior, so consumers should not infer kind or side effects from a tool name. WebFetch accepts only http: and https: by default and rejects loopback, link-local, private, reserved, and DNS-resolved non-public addresses at connect time and after every redirect. Configure allowedHosts and blockedHosts through SessionOptions.webFetch or BladeConfig.webFetch. allowPrivateNetwork: true is intended only for trusted local deployments. Bash narrows only simple, explicitly allowlisted commands to read-only. Pipelines, redirections, heredocs, variable or command substitution, eval, nested shells, and unknown commands remain side-effecting. Classification informs permissions and scheduling; it is not a security sandbox.
Select tools
ts
const session = await createSession({
provider,
model,
tools: [searchDocs],
allowedTools: ['Read', 'Glob', 'Grep', 'SearchDocs'],
disallowedTools: ['Bash'],
});An omitted allowedTools means no allowlist restriction. An empty array disables all tools.
Source policy
ts
toolSourcePolicy: {
allowedSources: ['builtin', 'custom'],
allowedTrustLevels: ['trusted', 'workspace'],
}- Built-in tools use
trusted. SessionOptions.toolsuseworkspace.- Remote MCP tools use
remote.