
Security News
pnpm 11.5 Adds Support for Recognizing npm Staged Publishes
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.
23 AI agent tools + context-compaction helper as standalone Vercel AI SDK modules
File operations, shell execution, code search, web fetching, and more -- everything an AI agent needs to interact with a codebase and system.
wrapLanguageModel(), preserves system messages and recent turnsgenerateText(), streamText(), and any AI SDK provider (OpenAI, Anthropic, Google, etc.)createBash({ cwd: '/my/project' }) for custom config, or just use bash with zero configexports mapanyexecute() returns a descriptive error string instead of throwingnpm install agentool ai zod
aiandzodare peer dependencies. You also need an AI SDK provider like@ai-sdk/openai,@ai-sdk/anthropic, etc.
rg) -- required for grep and glob tools# macOS
brew install ripgrep
# Ubuntu/Debian
sudo apt install ripgrep
# Windows
choco install ripgrep
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { bash, read, edit, glob, grep } from 'agentool';
const { text } = await generateText({
model: openai('gpt-4o'),
tools: { bash, read, edit, glob, grep },
maxSteps: 10,
prompt: 'Find all TypeScript files with TODO comments and list them',
});
// Only import what you need -- no unused code loaded
import { bash } from 'agentool/bash';
import { grep } from 'agentool/grep';
Use factory functions when you need to configure tools (custom cwd, timeouts, etc.):
import { createBash } from 'agentool/bash';
import { createRead } from 'agentool/read';
const myBash = createBash({ cwd: '/my/project', timeout: 60000 });
const myRead = createRead({ cwd: '/my/project' });
// Use them like any other tool
const { text } = await generateText({
model: openai('gpt-4o'),
tools: { bash: myBash, read: myRead },
maxSteps: 10,
prompt: 'List all files and read package.json',
});
Execute shell commands with timeout and signal handling.
import { bash } from 'agentool/bash';
const result = await bash.execute(
{ command: 'echo hello && ls -la' },
{ toolCallId: 'id', messages: [] },
);
With custom config:
import { createBash } from 'agentool/bash';
const bash = createBash({
cwd: '/my/project',
timeout: 60000, // 60s timeout (default: 120s)
shell: '/bin/zsh', // custom shell
});
Parameters: command (string), timeout? (number), description? (string)
Read files with line numbers, offset/limit pagination, and dual-path reading (fast for <10MB, streaming for larger).
import { read } from 'agentool/read';
// Read entire file
const content = await read.execute(
{ file_path: '/app/src/index.ts' },
{ toolCallId: 'id', messages: [] },
);
// Returns: "1\texport function hello() {\n2\t return 'world';\n3\t}"
// Read specific range
const range = await read.execute(
{ file_path: '/app/src/index.ts', offset: 10, limit: 20 },
{ toolCallId: 'id', messages: [] },
);
Parameters: file_path (string), offset? (number), limit? (number)
Exact string replacement with curly-quote normalization fallback.
import { edit } from 'agentool/edit';
const result = await edit.execute(
{
file_path: '/app/src/config.ts',
old_string: 'const PORT = 3000;',
new_string: 'const PORT = 8080;',
},
{ toolCallId: 'id', messages: [] },
);
// Replace all occurrences
const resultAll = await edit.execute(
{
file_path: '/app/src/config.ts',
old_string: 'localhost',
new_string: '0.0.0.0',
replace_all: true,
},
{ toolCallId: 'id', messages: [] },
);
Parameters: file_path (string), old_string (string), new_string (string), replace_all? (boolean)
Write files with automatic parent directory creation.
import { write } from 'agentool/write';
const result = await write.execute(
{
file_path: '/app/src/utils/helpers.ts',
content: 'export function add(a: number, b: number) {\n return a + b;\n}\n',
},
{ toolCallId: 'id', messages: [] },
);
// Returns: "Created file: /app/src/utils/helpers.ts (62 bytes)"
Parameters: file_path (string), content (string)
Search file contents with ripgrep. Three output modes, context lines, pagination.
import { grep } from 'agentool/grep';
// Find matching lines with context
const content = await grep.execute(
{ pattern: 'TODO|FIXME', output_mode: 'content', '-C': 2 },
{ toolCallId: 'id', messages: [] },
);
// List files containing matches (sorted by mtime)
const files = await grep.execute(
{ pattern: 'import.*react', output_mode: 'files_with_matches', glob: '*.tsx' },
{ toolCallId: 'id', messages: [] },
);
// Count matches per file
const counts = await grep.execute(
{ pattern: 'console\\.log', output_mode: 'count' },
{ toolCallId: 'id', messages: [] },
);
Parameters: pattern (string), path? (string), output_mode? ('content' | 'files_with_matches' | 'count'), glob? (string), type? (string), -i? (boolean), -n? (boolean), -A? (number), -B? (number), -C? / context? (number), head_limit? (number), offset? (number), multiline? (boolean)
Find files by pattern with ripgrep, sorted by modification time.
import { glob } from 'agentool/glob';
const result = await glob.execute(
{ pattern: '**/*.test.ts' },
{ toolCallId: 'id', messages: [] },
);
// Returns: "Found 27 files\n/app/tests/unit/bash.test.ts\n..."
// Search in specific directory
const result2 = await glob.execute(
{ pattern: '*.json', path: '/app/config' },
{ toolCallId: 'id', messages: [] },
);
Parameters: pattern (string), path? (string)
Atomically apply multiple edits to a single file. All succeed or none are applied.
import { multiEdit } from 'agentool/multi-edit';
const result = await multiEdit.execute(
{
file_path: '/app/src/config.ts',
edits: [
{ old_string: 'const PORT = 3000;', new_string: 'const PORT = 8080;' },
{ old_string: "const HOST = 'localhost';", new_string: "const HOST = '0.0.0.0';" },
],
},
{ toolCallId: 'id', messages: [] },
);
Parameters: file_path (string), edits (array of { old_string, new_string })
Generate unified diffs between files or strings.
import { diff } from 'agentool/diff';
// Compare two files
const fileDiff = await diff.execute(
{ file_path: '/app/old.ts', other_file_path: '/app/new.ts' },
{ toolCallId: 'id', messages: [] },
);
// Compare strings
const stringDiff = await diff.execute(
{ old_content: 'hello world', new_content: 'hello universe' },
{ toolCallId: 'id', messages: [] },
);
Parameters: file_path? (string), other_file_path? (string), old_content? (string), new_content? (string)
Fetch URLs with automatic HTML-to-markdown conversion.
import { webFetch } from 'agentool/web-fetch';
const result = await webFetch.execute(
{ url: 'https://example.com' },
{ toolCallId: 'id', messages: [] },
);
// Returns markdown content (HTML converted via Turndown, truncated at 100K chars)
Parameters: url (string, must be valid URL)
Search the web with a callback-based implementation (bring your own search provider).
import { createWebSearch } from 'agentool/web-search';
const webSearch = createWebSearch({
onSearch: async (query, { allowed_domains, blocked_domains }) => {
// Use Tavily, SerpAPI, Google, or any search provider
const results = await mySearchProvider.search(query, { allowed_domains, blocked_domains });
return results.map(r => `${r.title}: ${r.url}\n${r.snippet}`).join('\n\n');
},
});
const result = await webSearch.execute(
{ query: 'TypeScript best practices 2024', allowed_domains: ['typescript-eslint.io'] },
{ toolCallId: 'id', messages: [] },
);
Parameters: query (string, min 2 chars), allowed_domains? (string[]), blocked_domains? (string[])
Search through a registry of available tools by name or keyword.
import { createToolSearch } from 'agentool/tool-search';
const toolSearch = createToolSearch({
tools: {
bash: { description: 'Execute shell commands' },
grep: { description: 'Search file contents with regex' },
read: { description: 'Read file contents' },
},
});
const result = await toolSearch.execute(
{ query: 'file', max_results: 3 },
{ toolCallId: 'id', messages: [] },
);
// Returns matching tools sorted by relevance
Parameters: query (string), max_results? (number, default 5)
Validate the exact final JSON response against a JSON Schema configured by the application.
import { createOutputValidator } from 'agentool/output-validator';
const outputValidator = createOutputValidator({
schemaId: 'answer-v1',
schema: {
type: 'object',
additionalProperties: false,
required: ['answer', 'confidence'],
properties: {
answer: { type: 'string' },
confidence: { type: 'number', minimum: 0, maximum: 1 },
},
},
example: { answer: 'Use the validator per turn.', confidence: 0.92 },
errorMode: 'first-per-path',
});
const result = await outputValidator.execute(
{ content: '{"answer":"Use the validator per turn.","confidence":0.92}' },
{ toolCallId: 'id', messages: [] },
);
// Returns JSON: { "valid": true, "schemaId": "answer-v1", ... }
Use a fresh validator instance for the current turn's schema:
const tools = {
output_validator: createOutputValidator({
schemaId: 'current-turn-output',
schema: currentTurnSchema,
}),
};
If the schema changes on the next turn, create a new validator and pass it under the same tool name. No new chat session is required as long as your app rebuilds the tool list for that model call.
Validation errors include path, message, keyword, Ajv metadata, and instanceValue when the failing JSON value can be resolved. instanceValue is compact JSON truncated to about 200 characters.
Parameters: content (string, exact final JSON response text)
Config: schema, schemaId?, ajvOptions?, description?, example?, errorMode? ('all' | 'first-per-path' | 'first', default 'first-per-path')
Make raw HTTP requests without markdown conversion.
import { httpRequest } from 'agentool/http-request';
const result = await httpRequest.execute(
{
method: 'POST',
url: 'https://api.example.com/data',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
timeout: 5000,
},
{ toolCallId: 'id', messages: [] },
);
// Returns JSON: { status, statusText, headers, body }
Parameters: method ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'), url (string), headers? (object), body? (string), timeout? (number)
File-based key-value store for persistent agent memory.
import { memory } from 'agentool/memory';
// Write
await memory.execute(
{ action: 'write', key: 'user-prefs', content: 'Prefers dark mode' },
{ toolCallId: 'id', messages: [] },
);
// Read
const data = await memory.execute(
{ action: 'read', key: 'user-prefs' },
{ toolCallId: 'id', messages: [] },
);
// List all keys
const keys = await memory.execute(
{ action: 'list' },
{ toolCallId: 'id', messages: [] },
);
// Delete
await memory.execute(
{ action: 'delete', key: 'user-prefs' },
{ toolCallId: 'id', messages: [] },
);
Parameters: action ('read' | 'write' | 'list' | 'delete'), key? (string), content? (string)
Create a new task with subject, description, and optional metadata.
import { taskCreate } from 'agentool/task-create';
const result = await taskCreate.execute(
{ subject: 'Fix login bug', description: 'Auth fails on refresh', metadata: { priority: 'high' } },
{ toolCallId: 'id', messages: [] },
);
Parameters: subject (string), description (string), metadata? (Record<string, unknown>)
Retrieve a task by ID to see full details.
import { taskGet } from 'agentool/task-get';
const result = await taskGet.execute(
{ taskId: 'abc123' },
{ toolCallId: 'id', messages: [] },
);
Parameters: taskId (string)
Update a task's status, owner, metadata, and dependency relationships.
import { taskUpdate } from 'agentool/task-update';
// Update status and owner
await taskUpdate.execute(
{ taskId: 'abc123', status: 'in_progress', owner: 'agent-1' },
{ toolCallId: 'id', messages: [] },
);
// Add dependencies and merge metadata (null deletes a key)
await taskUpdate.execute(
{ taskId: 'abc123', addBlockedBy: ['def456'], metadata: { priority: null, notes: 'reviewed' } },
{ toolCallId: 'id', messages: [] },
);
Parameters: taskId (string), subject? (string), description? (string), status? ('pending' | 'in_progress' | 'completed' | 'deleted'), owner? (string), activeForm? (string), addBlocks? (string[]), addBlockedBy? (string[]), metadata? (Record<string, unknown>)
List all non-deleted tasks with status and dependencies.
import { taskList } from 'agentool/task-list';
const result = await taskList.execute(
{},
{ toolCallId: 'id', messages: [] },
);
Parameters: none
Language Server Protocol operations for code intelligence.
import { createLsp } from 'agentool/lsp';
const lsp = createLsp({
servers: {
'.ts': { command: 'typescript-language-server', args: ['--stdio'] },
'.py': { command: 'pylsp' },
},
});
const result = await lsp.execute(
{ operation: 'goToDefinition', filePath: 'src/index.ts', line: 10, character: 5 },
{ toolCallId: 'id', messages: [] },
);
Parameters: operation ('goToDefinition' | 'findReferences' | 'hover' | 'documentSymbol' | 'workspaceSymbol' | 'goToImplementation' | 'prepareCallHierarchy' | 'incomingCalls' | 'outgoingCalls'), filePath (string), line (number, 1-based), character (number, 1-based)
Breaking in 1.3.0: the
createContextCompactionmiddleware was removed in favor of a purecompactMessagesfunction. The middleware couldn't persist compacted state back to the caller, so every over-threshold turn re-summarized — costly and cache-busting. The function form returns the new messages and the caller assigns it back. See migration.
compactMessages summarizes older conversation history when usage crosses a threshold, preserving the leading system prefix and the most recent turns. It works with any provider the AI SDK supports (OpenAI / Anthropic / Google / Mistral / xAI / etc.) because it operates on the unified ModelMessage[] shape.
import { compactMessages } from 'agentool/context-compaction';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-5');
let messages: ModelMessage[] = [];
// Each turn:
messages.push({ role: 'user', content: userInput });
messages = await compactMessages({
messages,
summaryModel: openai('gpt-5-mini'), // cheap summarizer (can differ from main model)
maxContextTokens: 400_000,
});
const result = await generateText({ model, messages });
messages.push(...result.response.messages);
When usage is under threshold, compactMessages returns the same messages reference (===), so the second-call cost is cheap.
Output shape after compaction:
[ ...leadingSystemPrefix,
{ role: 'user', content: <summary> },
{ role: 'assistant', content: 'Understood.' },
...lastNMessages ]
The synthetic user → assistant ack pair preserves role alternation (required by Anthropic / Google providers).
Tool-chain safety: the recent-window boundary auto-extends backwards so tool-call / tool-result / tool-approval-request / tool-approval-response IDs are never split across the summarization boundary. No MissingToolResultsError from the AI SDK.
Cross-provider example (Anthropic):
import { anthropic } from '@ai-sdk/anthropic';
messages = await compactMessages({
messages,
summaryModel: anthropic('claude-sonnet-4-20250514'),
maxContextTokens: 200_000,
});
Custom summarizer (e.g. local model, cached, or anything else):
messages = await compactMessages({
messages,
maxContextTokens: 200_000,
summarize: async (older, targetTokens) => {
return callMyOwnSummarizer(older, targetTokens);
},
});
Options:
| Option | Type | Default | Description |
|---|---|---|---|
messages | ModelMessage[] | required | Conversation to compact |
maxContextTokens | number | required | Model's context window |
summaryModel or summarize | LanguageModelV3 or fn | required | Exactly one |
autoCompactThresholdPct | number (0-1) | 0.8 | Trigger threshold |
summaryTargetTokens | number | floor(maxContextTokens * 0.05) | Target summary size |
reservedOutputTokens | number | 16384 | Tokens reserved for output |
keepRecentMessages | number | 1 | Last N messages kept verbatim (extended for tool-chain safety) |
estimateTokens | (msgs) => number | char/4 heuristic | Custom token estimator (use a real tokenizer for accuracy) |
onCompactionFailure | 'passthrough' | 'throw' | 'passthrough' | What to do when summarization fails or returns oversize text |
Caveats:
estimateTokens with a provider-specific tokenizer (e.g. tiktoken for OpenAI, @anthropic-ai/tokenizer for Anthropic).system messages are NOT hoisted — only the leading contiguous system prefix is preserved as system.Before (v1.2.x):
import { createContextCompaction } from 'agentool/context-compaction';
import { wrapLanguageModel } from 'ai';
const model = wrapLanguageModel({
model: anthropic('claude-sonnet-4-20250514'),
middleware: createContextCompaction({ maxContextTokens: 200_000 }),
});
const { text } = await generateText({ model, messages });
After (v1.3.0):
import { compactMessages } from 'agentool/context-compaction';
const model = anthropic('claude-sonnet-4-20250514');
messages = await compactMessages({
messages,
summaryModel: model,
maxContextTokens: 200_000,
});
const { text } = await generateText({ model, messages });
Prompt the user for input during agent execution.
import { createAskUser } from 'agentool/ask-user';
const askUser = createAskUser({
onQuestion: async (question, options) => {
// Your UI logic to prompt the user
return 'User response here';
},
});
const answer = await askUser.execute(
{ question: 'Which database should I use?', options: ['PostgreSQL', 'MySQL', 'SQLite'] },
{ toolCallId: 'id', messages: [] },
);
Parameters: question (string), options? (string[])
Pause execution for rate limiting or polling intervals.
import { sleep } from 'agentool/sleep';
const result = await sleep.execute(
{ durationMs: 2000, reason: 'Waiting for deployment' },
{ toolCallId: 'id', messages: [] },
);
// Returns: "Slept for 2001ms. Reason: Waiting for deployment"
Parameters: durationMs (number, max 300000), reason? (string)
Spawn and manage parallel subagents from an orchestrator session.
import { openai } from '@ai-sdk/openai';
import { bash, grep, read } from 'agentool';
import { createAgent } from 'agentool/agent';
const agentTool = createAgent({
model: openai('gpt-4o'),
tools: { bash, grep, read },
agents: {
explorer: {
description: 'Explore one focused area of the codebase',
systemPrompt: 'You are a focused exploration subagent. Report findings with file references.',
},
},
});
const started = await agentTool.execute(
{
action: 'start',
agent: 'explorer',
prompt: 'Inspect src/auth and summarize the login flow',
description: 'auth flow',
},
{ toolCallId: 'id', messages: [] },
);
const finished = await agentTool.execute(
{ action: 'wait', mode: 'all', timeoutMs: 60000 },
{ toolCallId: 'id', messages: [] },
);
Parameters: action (start, wait, status, result, list, stop), plus action-specific fields.
Subagents do not receive the agent tool recursively, even if it is present in the configured toolset.
Every tool follows the factory + default pattern:
// Default instance -- uses process.cwd(), default timeouts
import { bash } from 'agentool/bash';
// Custom instance -- configure cwd, timeouts, and tool-specific options
import { createBash } from 'agentool/bash';
const myBash = createBash({
cwd: '/my/project',
timeout: 60000,
shell: '/bin/zsh',
});
All tools accept BaseToolConfig:
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | process.cwd() | Working directory for file operations |
Tools that support timeouts extend TimeoutConfig:
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | varies | Timeout in milliseconds |
| Tool | Extra Config |
|---|---|
bash | shell?: string -- shell binary path |
read | maxLines?: number -- max lines to return (default: 2000) |
memory | memoryDir?: string -- storage directory |
task-create | tasksFile?: string -- JSON file path |
task-get | tasksFile?: string -- JSON file path |
task-update | tasksFile?: string -- JSON file path |
task-list | tasksFile?: string -- JSON file path |
web-search | onSearch?: (query, opts) => Promise<string> -- search callback |
tool-search | tools?: Record<string, { description }> -- tool registry |
output-validator | schema?: JsonSchema, schemaId?: string, ajvOptions?: Record<string, unknown> |
lsp | servers?: Record<string, LspServerConfig> -- LSP servers by file extension |
http-request | defaultHeaders?: Record<string, string> -- headers merged into every request |
web-fetch | maxContentLength?: number, userAgent?: string |
ask-user | onQuestion?: (question, options?) => Promise<string> |
sleep | maxDuration?: number -- cap in ms (default: 300000) |
agent | model?: LanguageModel, tools?: ToolSet, agents?: Record<string, ManagedAgentDefinition>, maxConcurrent?: number |
Every tool's execute() catches errors internally and returns a descriptive string -- it never throws:
const result = await read.execute(
{ file_path: '/nonexistent/file.ts' },
{ toolCallId: 'id', messages: [] },
);
// Returns: "Error [read]: Failed to read file: ENOENT: no such file or directory..."
Error strings follow the format: Error [tool-name]: {description} with actionable context to help the AI model recover.
import {
bash, createBash,
read, createRead,
edit, createEdit,
write, createWrite,
grep, createGrep,
glob, createGlob,
webFetch, createWebFetch,
webSearch, createWebSearch,
toolSearch, createToolSearch,
outputValidator, createOutputValidator,
httpRequest, createHttpRequest,
memory, createMemory,
multiEdit, createMultiEdit,
diff, createDiff,
taskCreate, createTaskCreate,
taskGet, createTaskGet,
taskUpdate, createTaskUpdate,
taskList, createTaskList,
lsp, createLsp,
compactMessages, // helper function, not a tool
askUser, createAskUser,
sleep, createSleep,
agent, createAgent,
} from 'agentool';
import { bash } from 'agentool/bash';
import { grep } from 'agentool/grep';
import { glob } from 'agentool/glob';
import { read } from 'agentool/read';
import { edit } from 'agentool/edit';
import { write } from 'agentool/write';
import { webFetch } from 'agentool/web-fetch';
import { httpRequest } from 'agentool/http-request';
import { memory } from 'agentool/memory';
import { multiEdit } from 'agentool/multi-edit';
import { diff } from 'agentool/diff';
import { taskCreate } from 'agentool/task-create';
import { taskGet } from 'agentool/task-get';
import { taskUpdate } from 'agentool/task-update';
import { taskList } from 'agentool/task-list';
import { webSearch } from 'agentool/web-search';
import { toolSearch } from 'agentool/tool-search';
import { outputValidator } from 'agentool/output-validator';
import { lsp } from 'agentool/lsp';
import { compactMessages } from 'agentool/context-compaction'; // helper function
import { askUser } from 'agentool/ask-user';
import { sleep } from 'agentool/sleep';
import { agent } from 'agentool/agent';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { bash, read, edit, write, glob, grep, diff } from 'agentool';
const { text, steps } = await generateText({
model: openai('gpt-4o'),
tools: { bash, read, edit, write, glob, grep, diff },
maxSteps: 20,
system: `You are a coding assistant. You can read, search, edit, and write files.
Always read a file before editing it. Use grep to search for patterns.
Use glob to find files. Use bash for git, build, and test commands.`,
prompt: 'Find all console.log statements in src/ and replace them with proper logger calls',
});
console.log(`Completed in ${steps.length} steps`);
console.log(text);
| Dependency | Version | Required |
|---|---|---|
| Node.js | >= 18 | Yes |
ai (Vercel AI SDK) | >= 5.0.17 | Peer dependency |
zod | >= 3.23.0 | Peer dependency |
ripgrep (rg) | any | For grep/glob tools |
FAQs
23 AI agent tools + context-compaction helper as standalone Vercel AI SDK modules
We found that agentool demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.