@polyweave/tools (1.0.8)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/tools@1.0.8"@polyweave/tools": "1.0.8"About this package
@polyweave/tools
Composable tool-execution wrappers for Polyweave push-stream pipelines — schema-driven tool duplexes, sync/async/streaming routers, schema installers, re-entrant tool loops, and built-in workspace file/command tools.
Synopsis
import {
createToolDuplex,
createWriteFileToolDuplex,
createReadFileToolDuplex,
createRunCommandToolDuplex,
createToolRouter,
createAsyncToolRouter,
createStreamingToolRouter,
createToolInstaller,
createToolLoopWrapper
} from '@polyweave/tools';
const tool = createToolDuplex({
name: 'greet',
description: 'Returns a greeting',
capabilities: ['read'],
schema: {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name']
},
handler: (args) => ({ greeting: `Hello, ${args.name}` })
});
const router = createToolRouter(llmDuplex, {
greet: (args) => ({ greeting: `Hello, ${args.name}` })
});
Install
npm install @polyweave/tools
Why
LLM tool calling requires schema-driven dispatch with DESCRIBE protocol support, sync and async execution, streaming argument accumulation for partial TOOL_CALL arguments, schema installer injection for provider registration, and re-entrant tool loops that feed results back to the LLM as follow-up messages. This package provides all six as composable primitives built on @polyweave/core bidirectional wrappers and frame duplexes.
API
createToolDuplex(config)
Creates a frame-duplex tool adapter with schema introspection. Responds to DESCRIBE frames with the tool's schema and handles TOOL_CALL frames by executing the handler.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
config.name |
string |
No | 'unnamed' |
Tool name (for routing and error messages) |
config.description |
string |
No | 'Tool: <name>' |
Human-readable description |
config.capabilities |
string[] |
No | ['read'] |
Capability tags (mirrored in DESCRIPTION) |
config.transactional |
boolean |
No | false |
Whether the tool is transactional |
config.reversible |
boolean |
No | true |
Whether the tool supports undo |
config.schema |
object |
No | { type: 'object', properties: {}, required: [] } |
JSON Schema for arguments |
config.handler |
function |
No | (args, cb) => cb(null, {}) |
Tool implementation |
config.timeoutMs |
number |
No | 30000 |
Timeout in ms (0 to disable) |
config.async |
boolean |
No | auto-detected | Explicit sync/async flag; overrides handler.length detection |
Returns a duplex { source, sink, _toolName, _capabilities } created by createFrameDuplex.
Handler detection: if config.async is explicitly set, that value is used. Otherwise, if handler.length > 1 the handler is treated as async (callback-based); handler.length <= 1 means sync (return value).
Frame protocol:
| Input Frame | Action |
|---|---|
DESCRIBE |
Responds with DESCRIPTION frame (schema, capabilities, metadata), then END |
TICK, CANCEL, ABORT |
Ignored |
TOOL_CALL (matching name) |
Executes handler, responds with TOOL_RESULT then END |
TOOL_CALL (different name) |
Ignored |
TOOL_RESULT on success: content.result is the handler return value (or {} if undefined).
TOOL_RESULT on error: content.result = { success: false, error: err.message }.
Error conditions:
- Handler throws synchronously →
TOOL_RESULTwitherror: err.message - Timeout (
timeoutMs > 0) →TOOL_RESULTwitherror: 'Tool <name> timed out after <ms>ms' - Async handler calls callback with error →
TOOL_RESULTwitherror: err.message - The
toolCallIdandstreamIdare propagated from the triggeringTOOL_CALLframe
createWriteFileToolDuplex(workspaceDir, allowedExtensions)
Built-in duplex tool for writing files to a workspace directory.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
workspaceDir |
string |
Yes | — | Absolute path to the workspace root |
allowedExtensions |
string[] |
No | — | If provided, restricts allowed file extensions (e.g. ['.js', '.ts', '.md']) |
Tool name: write_file. Schema requires path (string) and content (string). Paths are relative to workspaceDir; .. sequences are stripped.
Returns { success: boolean, path: string, bytes: number } or { success: false, error: string }.
Error conditions: empty path, disallowed extension, or filesystem errors from mkdirSync/writeFileSync.
createReadFileToolDuplex(workspaceDir, allowedExtensions)
Built-in duplex tool for reading files from a workspace directory.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
workspaceDir |
string |
Yes | — | Absolute path to the workspace root |
allowedExtensions |
string[] |
No | — | If provided, restricts allowed file extensions |
Tool name: read_file. Schema requires path (string). An empty or blank path lists directory files.
Returns:
- For directory listing:
{ success: true, path: <workspaceDir>, isDirectory: true, files: string[], hint: string } - For file read:
{ success: true, path: string, content: string } - On error:
{ success: false, error: string, path: string }
Error conditions: disallowed extension or readFileSync/readdirSync filesystem errors.
createRunCommandToolDuplex(workspaceDir, timeoutMs)
Built-in duplex tool for executing shell commands in the workspace directory.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
workspaceDir |
string |
Yes | — | Working directory for the command |
timeoutMs |
number |
No | 30000 |
Command timeout in ms |
Tool name: run_command. Schema requires command (string). Uses execSync with maxBuffer: 1MB, stdio: ['ignore', 'pipe', 'pipe'].
Returns (always success: true — errors are captured):
- On success:
{ success: true, command: string, stdout: string, exitCode: 0 } - On failure:
{ success: true, command: string, stdout: string, stderr: string, exitCode: number } - On empty command:
{ success: false, error: string }
createToolRouter(innerDuplex, tools, options)
Synchronous tool router. Intercepts TOOL_CALL frames on the response path, executes the matching tool synchronously, and writes TOOL_RESULT back.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
object |
Yes | — | Duplex to wrap (LLM adapter response path) |
tools |
object |
No | {} |
Map of name → (args) => result |
options.name |
string |
No | null |
Wrapper name |
options.silent |
boolean |
No | false |
Suppress console output |
options.debug |
boolean |
No | false |
Enable debug logging |
Tools must be synchronous functions (args) => result. Unknown tools produce a TOOL_RESULT error frame. Tool exceptions are caught and reported as error frames. All intercepted TOOL_CALL frames return null (preventing pass-through).
Frame protocol:
| Direction | Frame Type | Action |
|---|---|---|
| Response | TOOL_CALL |
Executes tool sync → writes TOOL_RESULT to inner sink → returns null |
| All other | All types | Returned unchanged |
createAsyncToolRouter(innerDuplex, tools)
Async tool router. Same as createToolRouter but tools are callback-based: (args, callback) => void.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
object |
Yes | — | Duplex to wrap |
tools |
object |
No | {} |
Map of name → (args, callback) => void |
Tools must call callback(err, result). Exceptions on the sync invocation of the tool are caught; exceptions within the async callback body are not caught by the router.
createStreamingToolRouter(innerDuplex, tools, options)
Streaming-aware router. Supports atomic TOOL_CALL frames (same as sync router) plus streaming argument accumulation: TEXT frames with metadata.toolArgStream are buffered until a META frame with metadata.toolComplete signals completion.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
object |
Yes | — | Duplex to wrap |
tools |
object |
No | {} |
Map of name → (args) => result |
options.name |
string |
No | null |
Wrapper name |
options.durability |
object |
No | {} |
{ wrapperId, wrapperType, enabled } for ObservableState |
Returns a joined duplex { sink, source } that merges the bidirectional wrapper and ObservableState.
Frame protocol:
- Atomic
TOOL_CALL→ executed immediately (same ascreateToolRouter) - Streaming
TOOL_CALL(metadata.streaming) → registered as pending TEXTwithmetadata.toolArgStreamandmetadata.toolCallId→ chunk buffered intopendingToolCalls[].argumentChunksMETAwithmetadata.toolComplete→ chunks joined with'',JSON.parsed, tool executed, pending entry removedMETAwithdurability.restoreState→ internal, consumed byObservableState
Error conditions:
- Unknown tool →
TOOL_RESULTerror frame, pending entry removed JSON.parsefailure on accumulated arguments → not caught (will throw)- Tool exception →
TOOL_RESULTerror frame, pending entry removed
createToolInstaller(innerDuplex, tooling)
Installs tool schema, system prompt, and tool_choice into the first request's PARAM frames (one-shot injection).
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
object |
Yes | — | Duplex to wrap |
tooling.tools |
object[] |
No | [] |
Array of tool schemas to inject as PARAM { name: 'tools' } |
tooling.handlers |
object |
No | {} |
Map of name → (args, callback) => void for async tool execution |
tooling.systemPrompt |
string |
No | null |
System prompt prepended to messages |
tooling.toolChoice |
any |
No | null |
Tool choice parameter injected as PARAM { name: 'tool_choice' } |
On the first onRequest call, injects PARAM frames for tools, tool_choice, and (if first frame is a PARAM with name: 'messages') prepends systemPrompt as a system message. Subsequent requests pass through.
On the response path, intercepts TOOL_CALL frames and routes to tooling.handlers[name] (async callback-based).
createToolLoopWrapper(innerDuplex, options)
Re-entrant tool loop. Intercepts PARAM { name: 'messages' } to inject tool schemas and system prompt, then on each TOOL_CALL executes the handler and sends a follow-up message sequence to the inner duplex to keep the LLM iterating.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
object |
Yes | — | Duplex to wrap |
options.tools |
object[] |
No | [] |
Tool schemas injected as PARAM { name: 'tools' } |
options.handlers |
object |
No | {} |
Map of name → (args, callback) => void |
options.toolChoice |
any |
No | null |
Injected as PARAM { name: 'tool_choice' } |
options.systemPrompt |
string |
No | null |
Prepended to messages |
options.maxIterations |
number |
No | 3 |
Max tool-call iterations before forcing final answer |
options.finalSystemPrompt |
string |
No | 'Answer directly using the tool result. Do not call tools.' |
System message appended when max iterations reached |
options.onToolResult |
function |
No | null |
({ toolCall, result, streamId }) hook |
options.name |
string |
No | null |
Wrapper name |
On first PARAM { name: 'messages' }, injects tools, toolChoice, and systemPrompt. On TOOL_CALL, executes the handler then sends a full follow-up: previous PARAM values (excluding messages/tools/tool_choice), the tools array, tool_choice, and the updated message list with assistant tool_calls and tool role messages. After maxIterations, appends finalSystemPrompt to force the LLM to produce a final answer.
Frame Protocol Summary
| Module | Intercepts | Emits |
|---|---|---|
createToolDuplex |
DESCRIBE, TOOL_CALL |
DESCRIPTION, TOOL_RESULT, END |
createToolRouter |
TOOL_CALL (response) |
TOOL_RESULT (into inner sink) |
createAsyncToolRouter |
TOOL_CALL (response) |
TOOL_RESULT (into inner sink) |
createStreamingToolRouter |
TOOL_CALL, TEXT (tool arg stream), META (tool complete) |
TOOL_RESULT, META (state) |
createToolInstaller |
First request (injects params), TOOL_CALL (response) |
PARAM, TOOL_RESULT |
createToolLoopWrapper |
PARAM (messages), TOOL_CALL |
PARAM, TEXT, TOOL_RESULT |
Tests
npm test
Runs via node --test test/installer.test.js test/toolDuplex.test.js test/toolLoop.test.js test/workspace-duplex-tools.test.js. Covers: DESCRIBE protocol, TOOL_CALL dispatch (sync and async), timeout enforcement, error handling, workspace file I/O tools, installer injection, tool loop follow-up messaging, iteration limits, and streaming argument accumulation.
Requirements
- Node.js >= 22.0.0
- ESM only
@polyweave/core(peer)
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |
Peer Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |