Tools - agbossjs/agboss GitHub Wiki

Tools

A tool is a @Tool class in its workflow's tools/ folder โ€” see Components for the pattern (constructor dependencies, ToolHandler, tests with new).

import { Tool, type ToolContext, type ToolHandler } from "agboss";
import { ListOf, Text } from "agboss/dto";

export class Issue {
  @Text() title!: string;
  @Text() url!: string;
}

export class IssueQuery {
  @Text({ prompt: "words to search the issues for" })
  query!: string;
}

export class IssueList {
  @ListOf(Issue)
  issues!: Issue[];
}

@Tool({
  name: "search_issues",
  description: "Search the team's issues",
  input: IssueQuery,
  output: IssueList,
  timeoutMs: 10_000,
  deps: [ISSUES_API],
})
export class SearchIssues implements ToolHandler<IssueQuery, IssueList> {
  constructor(private readonly api: IssuesApi) {}

  run({ query }: IssueQuery, ctx: ToolContext): Promise<IssueList> {
    return this.api.search(query, ctx.run.signal);
  }
}

Input and output are DTO classes (Components โ†’ DTOs); the model sees the input DTO's JSON Schema, with each field's prompt as its description.

Contract

Step of tool.invoke(raw, ctx) On failure
input validated against the input DTO error: invalid input: <path>: <issue> (jobs[2].url) โ€” run is not called
run is not started if the run is already cancelled error: <abort reason>
run under a timeout and the run's AbortSignal error: timed out after N ms / error: <message>
output validated against the output DTO error: invalid output: โ€ฆ โ€” a contract bug of the tool

Results are values โ€” ToolResult = { kind: "ok", value } | { kind: "error", message } โ€” never exceptions: the model sees Tool error: โ€ฆ and can recover.

ToolContext gives a tool its run (ctx.run: runId, threadId, signal, metadata, owner), workflow, agent, callId, reportCost(usd) for paid tools, pause(question) and the caller's executionContext (Context and Authorization).

Library and workflow tools

A tool lives in its workflow's tools/ folder (the framework ships none). There is no central registry: an agent's tools: [SearchIssues] is the registration.

MCP

A server declares the tools it offers that the workflow uses (@McpServer({ tools }), extends McpServerClient); an @McpTool is a tool with that server injected, calling it in run โ€” see Components โ†’ MCP. Env variable NAMES only in the config, never secrets. A server tool that answers with plain text declares output: PlainText (agboss/dto, the one field text) โ€” the server's text arrives as { text }; agboss g mcp writes that by default.

Agents with tools

// agents/scout.agent.ts โ€” the workflow sets defaults.tools.maxToolCalls; an agent may override it
@Agent({
  name: "scout",
  description: "Finds jobs",
  promptUrls: ["./scout.prompt.md"],
  model: "moonshotai/kimi-k2.6",
  tools: [GreenhouseJobs],
  maxToolCalls: 4,
})
export class Scout {}
  • tools lists classes โ€” a missing or not-imported tool does not compile; at startup names are re-checked against the registry (UnknownAgentToolError).
  • The agent runs the framework's own loop (Agents): usage recorded per model call, the run budget checked before each call (spent โ†’ stopped: run budget exhausted, then the run fails at limits.perRun.cost), maxToolCalls โ†’ agents.<name>.limits.toolCalls (default 20) fails the run with LimitExceededError; the loop's spend still reaches the daily ledger.
  • Each call runs as its own task with the run's signal and the tool's timeoutMs; parallel calls of one turn run concurrently.
  • ctx.callId is the call's id โ€” the same when a call interrupted by a crash runs again. Tools with side effects must use it as their idempotency key.
  • Tool errors do not fail the run: the model reads Tool error: โ€ฆ and continues.

For the model

toolDefinitionOf(tool) describes a tool to the agent's chat model (bindTools): name, description and the JSON Schema of the input; validation, timeout and error mapping stay in invoke.

Isolation and tests

src/tools imports only config, finops, llm; the rest of the code imports it only through src/tools/index.ts (ESLint) โ€” inside the framework; tool tests run without network.

Decisions: ADR-0003.

Who can use which tool

An agent's tools lists class references, so a missing or not-imported tool is a compile error. To see it all at once โ€” no API key or network needed:

npm run describe
  notes  moonshotai/kimi-k2.6 ยท thinking default ยท history 5 turns + 10 summaries
    ยท note_search (read, local) โ† NOTES_FILE โ€” Find the user's saved notes โ€ฆ
    ยท note_save (write, local, waits for approval) โ† NOTES_FILE โ€” Save a note for the user.
unassigned tools: none

Testing

A tool is a plain class: construct it with fakes and call it through the same adapter the agent loop uses โ€” toolOf validates the input and output DTOs and maps errors exactly like a real call:

import { expect, test } from "vitest";
import { testRunContext, toolOf } from "agboss/testing";

test("counts the issues the API found", async () => {
  const fakeApi = { search: () => Promise.resolve([{ title: "a" }, { title: "b" }]) };
  const tool = toolOf(new CountIssues(fakeApi));

  const result = await tool.invoke({ query: "login" }, {
    run: testRunContext(),
    runId: "run-1",
    workflow: "desk",
    agent: "support",
    callId: "call-1",
    signal: new AbortController().signal,
    reportCost: () => undefined,
    pause: () => undefined,
  });

  expect(result).toEqual({ kind: "ok", value: { summary: "2 issues" } });
});

In a workflow test the tool's dependencies are replaced with mockOf(Service), and app.tool(CountIssues).invoke(input) runs the tool alone with the app's real (or mocked) dependencies (Testing).

โš ๏ธ **GitHub.com Fallback** โš ๏ธ