04. Tool System & Built-in Tool Libraries
⭐️ Love Smoke Monkey Harness? Please support the project with a star on GitHub! 🌐 Interactive Simulator: Test the agent loop and stream live at smoke-monkey-harness.vercel.app
Tools give AI models the ability to act on the world: inspecting source code, making edits, executing test suites, querying databases, and communicating with developers.
In Smoke Monkey Harness, every tool is modeled as a strongly-typed ToolDefinition with JSON Schema validation, contextual execution, and visual presentation metadata for frontend rendering.
The ToolDefinition Contract
Every tool conforms to the following TypeScript interface:
export interface ToolDefinition {
name: string;
description: string;
inputSchema: Record<string, unknown>; // JSON Schema or Zod object
annotations?: {
title?: string;
readOnlyHint?: boolean;
};
// Presentational metadata for UI rendering (never sent to the LLM)
presentation?: {
label: string;
icon: string; // Emoji or SVG glyph key
tone?: 'info' | 'success' | 'warning' | 'error';
group?: string; // e.g. 'utilities' | 'database' | 'deployment'
};
execute(input: any, ctx: ToolContext): Promise<ToolResult>;
}
export interface ToolResult {
content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }>;
isError?: boolean;
}
Built-In Tool Groups
The harness includes 5 battle-tested tool groups optimized for autonomous code generation:
| Area | Tools | Description |
|---|---|---|
| Filesystem | read_file, write_file, edit_file, line_edit, replace_lines, apply_patch, delete_file, list_directory, inspect |
Precision code modification tools supporting exact line replacement, chunk edits, and unified diff patches. |
| Terminal | run_command, run_tests |
Sandboxed command execution with real-time stdout/stderr streaming and non-zero exit code capture. |
| Search | glob, grep |
Fast regex code search and file pattern matching across large repositories. |
| Git | git_status, git_diff, git_log |
Version control inspection to evaluate changes before and after edits. |
| Agent | ask_user, context_manage, finish_task, todo_write |
Agent meta-tools for asking human questions, managing system subcontexts, and updating task checklists. |
Selecting Tool Groups
By default, agent.run() registers all core groups. You can restrict active tools via options.tools:
import { createAgent, TOOL_GROUPS } from '@smoke-monkey/harness';
const agent = createAgent({
workspacePath: process.cwd(),
// Expose only search and git inspection tools (read-only safe mode)
tools: ['search', 'git'],
});
Writing Custom Tools
You can easily register proprietary tools (APIs, internal databases, cloud deployments) directly in options.tools:
import { createAgent, type ToolDefinition } from '@smoke-monkey/harness';
// 1. Define your custom tool
const fetchDbMetricsTool: ToolDefinition = {
name: 'fetch_db_metrics',
description: 'Fetches database connection pool and latency metrics for an environment.',
inputSchema: {
type: 'object',
properties: {
environment: {
type: 'string',
enum: ['staging', 'production'],
description: 'Target environment'
},
durationMinutes: {
type: 'number',
default: 15
},
},
required: ['environment'],
},
// Rich presentation configuration for @smoke-monkey/ui
presentation: {
label: 'DB Metrics',
icon: 'database',
tone: 'info',
group: 'database',
},
async execute(input, ctx) {
try {
const stats = await dbService.getMetrics(input.environment, input.durationMinutes);
return {
content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }],
};
} catch (err: any) {
return {
content: [{ type: 'text', text: `Failed to fetch DB metrics: ${err.message}` }],
isError: true, // Informs the LLM that the execution failed
};
}
},
};
// 2. Supply it during agent creation
const agent = createAgent({
workspacePath: process.cwd(),
tools: [fetchDbMetricsTool],
});
The Up-Front Registration Rule
Important: Tools must be registered during initialization or before
agent.run()begins.If a tool is dynamically registered mid-run, the LLM will not be aware of its JSON Schema in the active prompt, and any subsequent call to it will be rejected as unrecognized.
Rich UI Tool Presentations
The presentation property is completely decoupled from the LLM prompt:
- No Token Overhead: Presentational fields (
label,icon,tone,group) are stripped before schemas are sent to the provider. - Frontend Synchronization: When
@smoke-monkey/uiconnects, it callsagent.getToolPresentations()to register custom icons and labels before the first event arrives.
// Export all registered tool presentations to pass to @smoke-monkey/ui
const presentations = agent.getToolPresentations();
// { fetch_db_metrics: { label: 'DB Metrics', icon: 'database', tone: 'info', group: 'database' } }
In @smoke-monkey/ui:
<SmokeMonkeyChat
transport={transport}
toolPresentations={presentations}
/>
Next Steps
- Guard tool execution with approval gates: 05. Permissions
- Bring in external tools using the Model Context Protocol: 06. Model Context Protocol
- Implement security firewalls and secret redaction with hooks: 09. Lifecycle Hooks
Tools
⭐️ Love Smoke Monkey Harness? Please support the project with a star on GitHub! 🌐 Interactive Simulator: Test the agent loop and stream live at smoke-monkey-harness.vercel.app
Every tool is a ToolDefinition:
type ToolDefinition = {
name: string;
description: string;
inputSchema: JSONSchema; // zod-like or JSON Schema
annotations?: { title?: string; readOnlyHint?: boolean };
execute(args: unknown, ctx: ToolContext) => Promise<ToolResult>;
};
ToolResult is { content: [{ type: 'text', text }] }, or
{ content, isError: true } for failures.
Built-in factories
Take (ctx: ToolContext) and return a ToolDefinition.
| Area | Factories |
|---|---|
| Filesystem | getReadFileTool getWriteFileTool getEditFileTool getLineEditTool getReplaceLinesTool getApplyPatchTool getDeleteFileTool getListDirectoryTool getInspectTool |
| Terminal | getRunCommandTool getRunTestTool |
| Search | getGlobTool getGrepTool |
| Git | getGitStatusTool getGitDiffTool getGitLogTool |
| Agent | getAskUserTool getContextManageTool getFinishTaskTool getTodoWriteTool |
| MCP | getInspectMcpStockTool getRequestMcpApprovalTool |
| Skills | getListSkillsTool getUseSkillTool |
Groups
Exported consts — TOOL_GROUPS.core, .filesystem, .terminal, .search,
.git, .agent, .mcp, .skills; plus READ_ONLY_TOOLS,
FILE_MUTATING_TOOLS, SEARCH_FAMILY_TOOLS, PHASE_TOOLS.
agent.run() registers the core 5 groups + active MCP/skill tools
automatically. Restrict with options.tools: ['core', 'search'].
Custom tools
Pass ToolDefinition[] in options.tools:
import { createAgent, buildToolRegistry } from '@smoke-monkey/harness';
const timeTool = {
name: 'current_time',
description: 'Return the current UTC time',
inputSchema: { type: 'object', properties: {} },
async execute() {
return { content: [{ type: 'text', text: new Date().toISOString() }] };
},
};
const agent = createAgent({ provider: 'nvidia', model: '...',
workspacePath: process.cwd(), tools: [timeTool] });
Tools passed this way are exposed to the model from the first run step. If a
tool is registered after the run begins (agent.registerTool() inside a hook,
say) the model never sees it, and a call to it is rejected as unexposed — so
register custom tools up front.
Presenting a tool
Add presentation to give the UI a titled, iconed tool card:
const timeTool = {
name: 'current_time',
description: 'Return the current UTC time',
inputSchema: { type: 'object', properties: {} },
presentation: { label: 'Time', icon: 'clock', tone: 'info', group: 'utilities' },
async execute() { /* … */ },
};
| field | meaning |
|---|---|
label |
title shown on the card |
icon |
icon key the host maps to its own set |
tone |
success | error | warning | info — drives the accent |
group |
optional grouping for the host's own UI |
It is a plain serialisable object, so it survives the trip to a browser, and it
is never sent to the model. family is an older alias for group and still
resolves. Read everything registered with:
agent.getToolPresentations(); // { current_time: { label, icon, tone, group } }
Every tool.* event also carries toolName, and tool.started plus each
tool.completed carry presentation, so a card keeps its identity even when
the call errors. A presentation on the individual event takes precedence over
the registered one.
Blocking a tool call
Deny a call from a beforeToolCall hook by returning block: true. The reason
is fed back to the model as the tool's result, so the run recovers instead of
dying:
createAgent({
// …
hooks: {
beforeToolCall: ({ toolName, input }) => {
if (toolName === 'write_file' && !isInsideWorkspace(input.path)) {
return { block: true, reason: 'path is outside the workspace' };
}
// Return nothing to allow the call unchanged.
},
},
});
A blocked call is a policy decision, not a crash. It is reported to the
afterToolCall hook with blocked: true and an error carrying your reason,
which is what lets an audit tell a refusal apart from a genuine failure. The
UI sees the same thing as any other tool error — there is no separate
"blocked" event.
beforeToolCall can also return { input } to rewrite the arguments before
the tool runs, which is the hook's other job.
Skills (SKILL.md)
Skills bundle instructions into <dir>/<skill>/SKILL.md or <dir>/<skill>.md
with name / description YAML frontmatter — the format used by Claude Code,
Codex, and opencode.
- Discovery:
.opencode/,.claude/,.codex/skills/folders under the workspace and home; or setoptions.skillsDir. - The loop only exposes a one-line catalog (
list_skills). The model pulls the full body withuse_skill(id), which injects the## Skill:block into the next turn — just-in-time.
import { loadSkillsFromDirs } from '@smoke-monkey/harness';
const skills = loadSkillsFromDirs([`${process.cwd()}/.mine/skills`]);
const agent = createAgent({ /* … */, skills });
Permissions
Read-only tools auto-allow. Mutating tools (file writes, terminal commands) and
MCP servers pause for approval unless autoApprove: true.