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 here

JSON Description Format

Top-Level Fields

FieldTypeRequiredDescription
slugStringYesUnique identifier for the tool (no spaces)
descriptionStringYesHuman-readable description for the LLM
argsArrayYesList of argument definitions
empty-resultObjectNoBehavior when tool returns empty output

Argument Definition

Each argument in the args array has the following fields:

FieldTypeRequiredDescription
nameStringYesArgument name (used in JSON input)
descriptionStringYesDescription for the LLM
typeStringYesSemantic type (e.g., "string", "number")
backing_typeStringYesImplementation type (e.g., "string")
arityStringYes"single" or "optional"
modeStringYesHow to pass the argument (see below)

Argument Modes

The mode field determines how arguments are passed to the script:

ModeBehaviorExample
positionalAdded as consecutive argumentsscript run value
dashdashspaceAdded as --name value (two args)script run --name value
dashdashequalAdded as --name=value (one arg)script run --name=value
stdinConcatenated to stdin with blank separatorsecho "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

VariableDescriptionExample Value
AGENT_SESSION_IDUUID of the current session550e8400-e29b-41d4-a716-446655440000
AGENT_CONVERSATION_IDUUID of the conversation6ba7b810-9dad-11d1-80b4-00c04fd430c8
AGENT_TURN_IDUUID of the current turn7c8b9d20-beef-22e2-91c5-11d15fe541d9
AGENT_AGENT_IDUUID of the executing agent (if available)a1b2c3d4-e5f6-7890-abcd-ef1234567890
AGENT_SESSION_JSONFull 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"
fi

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

  1. Check if the variable is set before using it
  2. Handle potentially large JSON payloads efficiently
  3. Consider using jq or 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:

  1. Lists all executable files in the tools directory
  2. Calls each with describe to load its interface
  3. 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

  1. Always validate arguments - Check for missing or invalid arguments before processing
  2. Write to stdout - The LLM receives stdout; use stderr for errors
  3. Return non-zero on errors - Use appropriate exit codes
  4. Keep descriptions concise - LLMs work better with clear, brief descriptions
  5. Use appropriate modes - Choose argument modes that make sense for your tool’s interface
  6. Handle empty results gracefully - Consider using empty-result to provide helpful messages
  7. Document context usage - If your tool uses environment variables, document it for users
  8. 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 error

Use 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=openai

The --tools option accepts the following modes:

ModeDescription
noneDefault. Only show agent names and tool counts.
listPrint a markdown list of tool names and descriptions.
agents-exeOutput the internal tool representation as JSON within <details> tags.
openaiOutput 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