⭐️ Love Smoke Monkey Harness? Support our open source project! Star on GitHub ⭐ • 🌐 Interactive Agent Simulator: smoke-monkey-harness.vercel.app ↗
🐒 Smoke Monkey Harness v0.1.3

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:

ts
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:

ts
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:

ts
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:

ts
// 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:

tsx
<SmokeMonkeyChat 
  transport={transport} 
  toolPresentations={presentations} 
/>

Next Steps

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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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.

ts
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.