KODE SDK provides a comprehensive tool system with built-in tools, custom tool definition APIs, and MCP integration. All tools follow these conventions:
- Prompt Instructions: Each tool includes detailed prompts guiding the model's safe usage
- Structured Returns: Tools return JSON structures (e.g.,
fs_readreturns{ content, offset, limit, truncated }) - FilePool Integration: File tools automatically validate and record through FilePool, preventing freshness conflicts
- Audit Trail: ToolCallRecord captures approval, duration, and errors, fully restored on Resume
| Tool | Description | Returns |
|---|---|---|
fs_read |
Read file segment | { path, offset, limit, truncated, content } |
fs_write |
Create/overwrite file with freshness validation | { ok, path, bytes, length } |
fs_edit |
Precise text replacement (supports replace_all) |
{ ok, path, replacements, length } |
fs_glob |
Match files using glob patterns | { ok, pattern, cwd, matches, truncated } |
fs_grep |
Search text/regex in files or wildcard sets | { ok, pattern, path, matches[] } |
fs_multi_edit |
Batch edit multiple files | { ok, results[{ path, status, replacements, message? }] } |
recordRead/recordEdit: Track last read/write times for conflict detectionvalidateWrite: Verify file wasn't externally modified after Agent's last readwatchFiles: Auto-monitor file changes, triggersmonitor.file_changedevent
bash_run: Execute commands (foreground/background), controllable via Hooks orpermission.mode='approval'bash_logs: Read background command outputbash_kill: Terminate background commands
Recommended Security Strategy:
const agent = await Agent.create({
templateId: 'secure-runner',
sandbox: { kind: 'local', workDir: './workspace', enforceBoundary: true },
overrides: {
hooks: {
preToolUse(call) {
if (call.name === 'bash_run' && !/^git /.test(call.args.cmd)) {
return { decision: 'ask', meta: { reason: 'Non-whitelisted command' } };
}
return undefined;
},
},
},
}, deps);todo_read: Return Todo listtodo_write: Write complete Todo list (validates unique IDs, max 1 in-progress). Integrates withTodoManagerfor auto-reminders and events.
task_run: Delegate complex work to a sub-Agent selected from your template pool.- Parameters:
description: Short task title (recommended 3-5 words)prompt: Detailed instructions for the sub-AgentagentTemplateId: Must match a registered template IDcontext: Optional extra background (appended to the prompt)model: Optional model overridestring: keep parent provider, override model ID{ provider, model }: explicitly choose provider + model
- Return fields:
status:okorpausedtemplate: Template ID that was usedtext: Sub-Agent outputpermissionIds: Pending permission IDs (if any)
- Templates can restrict delegation depth and allowed template IDs via
runtime.subagents.
Minimal Example:
import { createTaskRunTool } from '@shareai-lab/kode-sdk';
const templates = [
{ id: 'researcher', system: 'Research and return structured findings.', whenToUse: 'Need search + analysis' },
{ id: 'writer', system: 'Turn findings into publishable copy.', whenToUse: 'Need final draft' },
];
const taskRunTool = createTaskRunTool(templates);
deps.toolRegistry.register('task_run', () => taskRunTool);
// Example tool-call args:
// {
// "description": "Research pricing",
// "prompt": "Analyze 3 competitors and provide a price table plus recommended range.",
// "agentTemplateId": "researcher",
// "context": "Target market: North America SMB",
// "model": { "provider": "openai", "model": "gpt-4.1-mini" }
// }Common Errors:
Agent template 'xxx' not found:agentTemplateIdis not in thecreateTaskRunTool(templates)list.- Delegation stops unexpectedly: check
runtime.subagentslimits (depth/allowed templates).
delegateTask Model Behavior (Important):
- In
task_run,modelis optional. If omitted, sub-Agent reuses parent Agent'sModelProviderinstance by default. - If you call
agent.delegateTask(...)directly, model resolution is:modelomitted: reuse parentModelProviderinstance (nomodelFactoryrequired)modelisstring: keep parent provider type and only override model ID (for custom providers, this path requiresmodelFactory)modelis{ provider, model }: explicitly choose provider + model (if provider differs from parent, custom providers usually requiremodelFactory)modelisModelProvider: use that instance directly
// Direct call with explicit model override
await agent.delegateTask({
templateId: 'researcher',
prompt: 'Analyze competitors and produce a pricing matrix.',
model: 'gpt-4.1', // same provider type as parent, model id overridden
});
⚠️ Note: Default Skills directory has changed fromskills/to.skills/. See Skills System Guide - Breaking Changes
skills: Load specific skill content (instructions, references, scripts, assets)- Parameters:
action: Operation type (currently onlyload,listoperation is disabled)skill_name: Skill name (required when action=load)
- Returns:
{ ok: true, data: { name: string, // Skill name (folder name) description: string, // Skill description content: string, // SKILL.md content base_dir: string, // Skill base directory references: string[], // Reference document list scripts: string[], // Available scripts assets: string[] // Asset files } }
- Parameters:
See skills.md for complete Skills system documentation.
The simplified API (v2.7+) auto-generates JSON Schema from parameter definitions:
import { defineTool } from '@shareai-lab/kode-sdk';
const weatherTool = defineTool({
name: 'get_weather',
description: 'Get weather information',
// Concise parameter definition - auto-generates Schema
params: {
city: {
type: 'string',
description: 'City name'
},
units: {
type: 'string',
description: 'Temperature units',
enum: ['celsius', 'fahrenheit'],
required: false,
default: 'celsius'
}
},
// Simplified attributes
attributes: {
readonly: true, // Read-only tool
noEffect: true // No side effects, safe to retry
},
async exec(args, ctx) {
// Custom events
ctx.emit('weather_fetched', { city: args.city });
return { temperature: 22, condition: 'sunny' };
}
});import { defineTools } from '@shareai-lab/kode-sdk';
const calculatorTools = defineTools([
{
name: 'add',
description: 'Add two numbers',
params: {
a: { type: 'number' },
b: { type: 'number' }
},
attributes: { readonly: true, noEffect: true },
async exec(args, ctx) {
return args.a + args.b;
}
},
{
name: 'multiply',
description: 'Multiply two numbers',
params: {
a: { type: 'number' },
b: { type: 'number' }
},
attributes: { readonly: true, noEffect: true },
async exec(args, ctx) {
return args.a * args.b;
}
}
]);For fine-grained control, use the classic interface:
const registry = new ToolRegistry();
registry.register('greet', () => ({
name: 'greet',
description: 'Greet a person by name',
input_schema: {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name']
},
prompt: 'Use this tool to greet teammates by name.',
async exec(args) {
return `Hello, ${args.name}!`;
},
toDescriptor() {
return { source: 'registered', name: 'greet', registryId: 'greet' };
},
}));params: {
str: { type: 'string', description: 'A string' },
num: { type: 'number', description: 'A number' },
bool: { type: 'boolean', description: 'A boolean' },
// Optional parameter
optional: { type: 'string', required: false },
// Default value
withDefault: { type: 'number', default: 42 },
// Enum
choice: {
type: 'string',
enum: ['option1', 'option2', 'option3']
}
}params: {
// Array
tags: {
type: 'array',
description: 'List of tags',
items: { type: 'string' }
},
// Nested object
profile: {
type: 'object',
description: 'User profile',
properties: {
email: { type: 'string' },
age: { type: 'number', required: false },
roles: {
type: 'array',
items: { type: 'string' }
}
}
}
}For constraints like pattern, minLength, use input_schema directly:
defineTool({
name: 'advanced_tool',
description: 'Advanced tool',
input_schema: {
type: 'object',
properties: {
data: {
type: 'string',
pattern: '^[A-Z]{3}$',
minLength: 3,
maxLength: 3
}
},
required: ['data']
},
async exec(args, ctx) {
// ...
}
});Indicates the tool doesn't modify any state (files, database, external APIs):
attributes: {
readonly: true
}Use Cases:
- Auto-approved in
readonlypermission mode - Suitable for queries, reads, computations
Indicates the tool can be safely retried with identical results:
attributes: {
noEffect: true
}Use Cases:
- Safe for re-execution on Resume
- Suitable for idempotent operations (GET requests, pure calculations)
Without attributes, tools are treated as:
- Non-readonly (may write)
- Has side effects (cannot retry)
defineTool({
name: 'process_data',
description: 'Process data',
params: { input: { type: 'string' } },
async exec(args, ctx: EnhancedToolContext) {
ctx.emit('processing_started', { input: args.input });
const result = await heavyComputation(args.input);
ctx.emit('processing_completed', { result, duration: 1234 });
return result;
}
});agent.on('tool_custom_event', (event) => {
console.log(`[${event.toolName}] ${event.eventType}:`, event.data);
});interface MonitorToolCustomEvent {
channel: 'monitor';
type: 'tool_custom_event';
toolName: string; // Tool name
eventType: string; // Custom event type
data?: any; // Event data
timestamp: number;
bookmark?: Bookmark;
}Default tool execution timeout is 60 seconds, customizable via Agent config:
const agent = await Agent.create({
templateId: 'my-assistant',
metadata: {
toolTimeoutMs: 120000, // 2 minutes
}
}, deps);All custom tools receive context.signal - must check in long-running operations:
export class MyLongRunningTool implements ToolInstance {
async exec(args: any, context: ToolContext) {
// Check before long operations
if (context.signal?.aborted) {
throw new Error('Operation aborted');
}
// Pass signal to underlying APIs
const response = await fetch(url, { signal: context.signal });
// Check periodically in loops
for (const item of items) {
if (context.signal?.aborted) {
throw new Error('Operation aborted');
}
await processItem(item);
}
return result;
}
}For pure computation tasks, actively check in loops:
for (let i = 0; i < args.iterations; i++) {
// Check every 100 iterations
if (i % 100 === 0 && context.signal?.aborted) {
throw new Error('Computation aborted');
}
result.push(this.compute(i));
}After timeout, Agent will:
- Send
abortsignal - Mark tool call as
FAILED - Generate
tool_resultwith timeout info - Continue to next
runStep
On Resume, timed-out tool calls are auto-sealed (Auto-Seal), not re-executed.
Register MCP loaders in ToolRegistry with registryId pointing to MCP service:
const registry = new ToolRegistry();
// Register MCP tool loader
registry.registerMCPLoader('my-mcp-server', async () => {
const client = await connectToMCPServer('my-mcp-server');
return client.getTools();
});Combined with TemplateRegistry, specify which templates enable MCP tools for proper Resume recovery.
- Always check
context.signal?.abortedin long-running operations - Pass signal to APIs supporting AbortSignal (fetch, axios, etc.)
- Set appropriate
attributesto help permission system - Use custom events for tool execution observability
- Prefer
defineTool()for cleaner, type-safe code - Use
input_schemaonly for advanced Schema constraints - Monitor timeout events for alerting
agent.on('error', (event) => {
if (event.phase === 'tool' && event.message.includes('aborted')) {
console.log('Tool execution timed out:', event.detail);
}
});| Legacy | New |
|---|---|
{ access: 'read', mutates: false } |
{ readonly: true } |
{ access: 'write', mutates: true } |
(default, no need to set) |
{ safe: true } |
{ noEffect: true } |
// Legacy - cannot emit events
async exec(args, ctx: ToolContext) {
return result;
}
// New - can emit events
async exec(args, ctx: EnhancedToolContext) {
ctx.emit('event_name', { data: 'value' });
return result;
}Q: Must I use the new API?
A: No, the legacy ToolInstance interface is fully compatible. The new API is optional enhancement.
Q: What's the difference between readonly and noEffect?
A:
readonly: Tool doesn't modify any state (files, database, etc.)noEffect: Tool can be safely retried with identical results
A read-only tool is usually also side-effect-free, but not vice versa.
Q: Are custom events persisted?
A: Yes, custom events are persisted to WAL as MonitorToolCustomEvent, recoverable on Resume.
Q: Can I mix old and new APIs?
A: Yes, freely mix them - Register tools in ToolRegistry and reference by name:
const tools = new ToolRegistry();
// Register different styles
tools.register('old_tool', () => oldStyleTool);
tools.register('new_tool', () => defineTool({ name: 'new_tool', /* ... */ }));
tools.register('fs_read', () => new FsRead());
// Reference in template
templates.register({
id: 'my-assistant',
tools: ['old_tool', 'new_tool', 'fs_read'],
});
const agent = await Agent.create({ templateId: 'my-assistant' }, deps);- Example code:
examples/tooling/simplified-tools.ts - Type definitions:
src/tools/define.ts - Event system: events.md