Tools - agbossjs/agboss GitHub Wiki
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.
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).
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.
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/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 {}-
toolslists 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 atlimits.perRun.cost),maxToolCallsโagents.<name>.limits.toolCalls(default 20) fails the run withLimitExceededError; 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.callIdis 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.
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.
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.
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
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).