Adding New Executable-Program Tools
On Sun, 04 Oct 2026, by @lucasdicioccio, 552 words, 9 code snippets, 0 links, 0images.
Generated from documentation/binary-tool.md, the repository is the canonical source and may be ahead of this page.
Adding New Executable-Program Tools
Executable-program tools (also called “bash tools”) are external programs that agents-exe can invoke as part of agent execution. These tools extend agent capabilities by performing side effects like reading files, installing packages, probing the network, or interacting with external APIs.
Tool Protocol Overview
Bash tools must adhere to a simple protocol with two commands:
1. The describe Command
When called with a single describe argument (no other parameters), the tool must return a JSON description of its interface:
$ ./my-tool describe
{
"slug": "my_tool",
"description": "What this tool does",
"args": [
{
"name": "arg_name",
"description": "Argument description",
"type": "string",
"backing_type": "string",
"arity": "single",
"mode": "dashdashspace"
}
],
"empty-result": { "tag": "AddMessage", "contents": "No results found" }
}2. The run Command
When called with run followed by arguments, the tool executes its function and must write output to stdout:
$ ./my-tool run --arg-name value
Tool output hereJSON Description Format
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
slug | String | Yes | Unique identifier for the tool (no spaces) |
description | String | Yes | Human-readable description for the LLM |
args | Array | Yes | List of argument definitions |
empty-result | Object | No | Behavior when tool returns empty output |
Argument Definition
Each argument in the args array has the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Argument name (used in JSON input) |
description | String | Yes | Description for the LLM |
type | String | Yes | Semantic type (e.g., "string", "number") |
backing_type | String | Yes | Implementation type (e.g., "string") |
arity | String | Yes | "single" or "optional" |
mode | String | Yes | How to pass the argument (see below) |
Argument Modes
The mode field determines how arguments are passed to the script:
| Mode | Behavior | Example |
|---|---|---|
positional | Added as consecutive arguments | script run value |
dashdashspace | Added as --name value (two args) | script run --name value |
dashdashequal | Added as --name=value (one arg) | script run --name=value |
stdin | Concatenated to stdin with blank separators | echo "value" \| script run |
Empty Result Behavior
The optional empty-result field controls what happens when the tool produces no output:
// Do nothing (default behavior)
{ "tag": "DoNothing" }
// Add a message when output is empty
{ "tag": "AddMessage", "contents": "No results found" }Session Context via Environment Variables
When a tool is executed within a session, agents-exe passes session context via environment variables. This allows tools to access session metadata without requiring the LLM to provide it as explicit arguments.
Available Environment Variables
| Variable | Description | Example Value |
|---|---|---|
AGENT_SESSION_ID | UUID of the current session | 550e8400-e29b-41d4-a716-446655440000 |
AGENT_CONVERSATION_ID | UUID of the conversation | 6ba7b810-9dad-11d1-80b4-00c04fd430c8 |
AGENT_TURN_ID | UUID of the current turn | 7c8b9d20-beef-22e2-91c5-11d15fe541d9 |
AGENT_AGENT_ID | UUID of the executing agent (if available) | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
AGENT_SESSION_JSON | Full session serialized as JSON (when requested) | {...} |
Use Cases for Context
Tools can use session context for:
- Audit logging: Include session IDs in external API calls for traceability
- Context-aware processing: Access conversation history to make smarter decisions
- Correlation: Tag external telemetry with session identifiers
- Agent-specific behavior: Adjust behavior based on which agent is executing the tool
Example: Tool with Session Context
#!/bin/bash
if [ "$1" == "describe" ]; then
cat <<'EOF'
{
"slug": "session-aware-logger",
"description": "Logs operations with session context for audit trails",
"args": [
{
"name": "operation",
"description": "The operation being logged",
"type": "string",
"backing_type": "string",
"arity": "single",
"mode": "dashdashspace"
}
]
}
EOF
exit 0
fi
# Access session context from environment (with defaults)
SESSION_ID="${AGENT_SESSION_ID:-unknown}"
CONVERSATION_ID="${AGENT_CONVERSATION_ID:-unknown}"
TURN_ID="${AGENT_TURN_ID:-unknown}"
AGENT_ID="${AGENT_AGENT_ID:-unknown}"
# Parse the operation argument (skipping 'run')
OPERATION=""
while [[ $# -gt 0 ]]; do
case $1 in
--operation)
OPERATION="$2"
shift 2
;;
*)
shift
;;
esac
done
# Log with context
echo "[session=$SESSION_ID turn=$TURN_ID] Executing: $OPERATION"
# Optionally access full session JSON for complex operations
if [ -n "$AGENT_SESSION_JSON" ]; then
# Parse session JSON for additional context
TURN_COUNT=$(echo "$AGENT_SESSION_JSON" | jq '.turns | length')
echo "[session=$SESSION_ID] Turn $TURN_COUNT"
fiNote on Session JSON
The AGENT_SESSION_JSON variable is only set when the full session context is available and requested. It contains the complete serialized session, which can be large. Tools should:
- Check if the variable is set before using it
- Handle potentially large JSON payloads efficiently
- Consider using
jqor similar for parsing
Complete Example Tool
Here’s a complete example of a file-reading tool:
#!/bin/bash
# File: tools/read-file.sh
if [ "$1" == "describe" ]; then
cat <<'EOF'
{
"slug": "read_file",
"description": "Reads the contents of a file at the specified path",
"args": [
{
"name": "path",
"description": "Absolute or relative path to the file to read",
"type": "string",
"backing_type": "string",
"arity": "single",
"mode": "dashdashspace"
}
],
"empty-result": { "tag": "AddMessage", "contents": "File is empty or could not be read" }
}
EOF
exit 0
fi
# Extract the path argument
FILE_PATH=""
while [[ $# -gt 0 ]]; do
case $1 in
--path)
FILE_PATH="$2"
shift 2
;;
*)
shift
;;
esac
done
if [ -z "$FILE_PATH" ]; then
echo "Error: No path specified" >&2
exit 1
fi
if [ ! -f "$FILE_PATH" ]; then
echo "Error: File not found: $FILE_PATH" >&2
exit 1
fi
cat "$FILE_PATH"Tool Discovery
Tools are discovered from the tools directory relative to your agent.json file:
project/
├── agent.json # Agent definition
└── tools/ # Tool directory
├── read-file.sh # Executable tool script
├── git-helper # Another tool
└── ...
agents-exe:
- Lists all executable files in the tools directory
- Calls each with
describeto load its interface - Makes successfully loaded tools available to the agent
Using agents-exe as a Tool
agents-exe itself adheres to the same protocol, allowing you to nest agents:
# This allows agents-exe to be used as a tool by another agents-exe instance
agents-exe describe
agents-exe run --prompt "Hello"This is useful for:
- Creating hierarchies of agents with different access rights
- Running agents across container boundaries
- Delegating to specialized sub-agents
Best Practices
- Always validate arguments - Check for missing or invalid arguments before processing
- Write to stdout - The LLM receives stdout; use stderr for errors
- Return non-zero on errors - Use appropriate exit codes
- Keep descriptions concise - LLMs work better with clear, brief descriptions
- Use appropriate modes - Choose argument modes that make sense for your tool’s interface
- Handle empty results gracefully - Consider using
empty-resultto provide helpful messages - Document context usage - If your tool uses environment variables, document it for users
- Provide sensible defaults - Use bash parameter expansion for optional context variables:
${AGENT_SESSION_ID:-unknown}
Debugging Tools
Test your tool independently before using it with agents-exe:
# Test the describe command
./my-tool describe | jq .
# Test the run command
./my-tool run --arg-name value
# Verify exit codes
echo $? # Should be 0 on success, non-zero on errorUse agents-exe check to validate all tools in your agent configuration.
Inspecting Tool Schemas
The check command supports an optional --tools flag that allows you to inspect how tools are serialized for LLM calls. This is useful for debugging tool definition issues:
# Default behavior - just validate and show tool counts
agents-exe check
# List tools with their names and descriptions
agents-exe check --tools=list
# Show the internal agents-exe tool representation as JSON (wrapped in markdown details tags)
agents-exe check --tools=agents-exe
# Show the OpenAI LLM tool representation as JSON (wrapped in markdown details tags)
agents-exe check --tools=openaiThe --tools option accepts the following modes:
| Mode | Description |
|---|---|
none | Default. Only show agent names and tool counts. |
list | Print a markdown list of tool names and descriptions. |
agents-exe | Output the internal tool representation as JSON within <details> tags. |
openai | Output the OpenAI LLM tool schema (as sent to the API) within <details> tags. |
This feature is particularly useful when:
- Debugging why a tool isn’t being called by the LLM
- Verifying that tool schemas are correctly formed
- Documenting available tools for users
- Comparing internal vs. LLM-facing representations