Skip to content

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:

  • sideEffect is optional. When it is omitted the tool counts as non_idempotent, so recovery never replays it. Declare ToolSideEffect.PURE or ToolSideEffect.IDEMPOTENT on read-only or safely repeatable tools to admit them into retryable recovery.
  • parameters accepts 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;
    };
  • model is written back to model context.
  • display is UI-facing content and should not be parsed to reconstruct model output.
  • data is optional caller-facing structured data and must be a strict JSON value. Large-result artifact persistence applies to model, not data.
  • Failed results require both status: 'error' and error.

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 sideEffect and interruptBehavior values 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 ExecutionContext filesystem 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:

  • block is the default. The tool completes before steering is applied.
  • cancel is for tools that observe context.signal and 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:

  1. onToolScheduled completes before tool_start is published.
  2. onPermissionRequested completes before the interactive confirmation handler runs.
  3. onPermissionResolved completes before the permission decision is accepted.
  4. onExecutionStarted persists the final post-permission input and resolved side-effect contract before invoking the tool generator, so a durable write failure blocks the side effect.
  5. onToolSettled completes before tool_result is 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.

GroupTools
FilesystemRead, Edit, Write, NotebookEdit, Glob, Grep
ShellBash, KillShell
WebWebFetch, WebSearch
SubagentsTask, TaskOutput
Structured tasksTaskCreate, TaskGet, TaskUpdate, TaskList, TaskStop
SystemAskUserQuestion, DiscoverTools, Skill
PlanningEnterPlanMode, ExitPlanMode
TodosTodoWrite
MemoryMemoryRead, MemoryWrite (requires SessionOptions.memoryManager)
MCP resourcesListMcpResources, 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:

ToolKindSide effect
ReadReadOnlypure
EditWritenon_idempotent
WriteWriteidempotent
NotebookEditWritenon_idempotent; replace narrows to idempotent
Glob, GrepReadOnlypure
BashExecutenon_idempotent; read-only foreground commands narrow to pure
KillShellExecuteidempotent
WebFetchExecutenon_idempotent; GET/HEAD narrow to pure, PUT/DELETE to idempotent
WebSearchReadOnlypure
TaskReadOnlynon_idempotent
TaskOutputReadOnlynon_idempotent
TaskCreateWritenon_idempotent
TaskGet, TaskListWritepure
TaskUpdate, TaskStopWriteidempotent
TodoWriteReadOnlyidempotent
MemoryReadReadOnlypure
MemoryWriteWriteidempotent
EnterPlanMode, ExitPlanMode, AskUserQuestionReadOnlynon_idempotent
DiscoverToolsReadOnlyidempotent
SkillExecutenon_idempotent
ListMcpResources, ReadMcpResourceReadOnlypure

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.tools use workspace.
  • Remote MCP tools use remote.

Released under the MIT License.