The ECS Agent framework provides a complete pipeline for tool management: automatic discovery, sandboxed execution, and human-in-the-loop approval.
The framework features an extensive event system for monitoring tool execution, skill lifecycles, and MCP connectivity. These events are published to the EventBus and can be used for logging, UI updates, or debugging.
| Event Name | Fired When | Key Fields |
|---|---|---|
ToolExecutionStartedEvent |
Before a tool handler is called | tool_call, entity_id |
ToolExecutionCompletedEvent |
After a tool handler returns | tool_call_id, result, success |
SkillInstalledEvent |
After a skill is installed | skill_name, tool_names, entity_id |
SkillUninstalledEvent |
After a skill is uninstalled | skill_name, entity_id |
SkillDiscoveryEvent |
After scanning a discovery source | source, skills_found, errors |
MCPConnectedEvent |
After an MCP server connects | server_name |
MCPDisconnectedEvent |
After an MCP server disconnects | server_name |
MCPToolCallEvent |
After an MCP tool is called | server_name, tool_name, success |
You can subscribe to these events to track the agent's progress in real-time.
async def on_tool_start(event: ToolExecutionStartedEvent):
print(f"Tool {event.tool_call.name} started execution.")
async def on_discovery(event: SkillDiscoveryEvent):
print(f"Discovered {len(event.skills_found)} skills from {event.source}")
world.event_bus.subscribe(ToolExecutionStartedEvent, on_tool_start)
world.event_bus.subscribe(SkillDiscoveryEvent, on_discovery)The scan_module function automatically discovers tool-decorated functions in a Python module.
from ecs_agent.tools import tool
@tool(
name="calculate",
description="Perform arithmetic calculations",
parameters={
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression to evaluate"}
},
"required": ["expression"]
}
)
async def calculate(expression: str) -> str:
return str(eval(expression))from ecs_agent.tools import scan_module
import my_tools_module
tools, handlers = scan_module(my_tools_module)
# tools: dict[str, ToolSchema] — tool name to schema mapping
# handlers: dict[str, Callable] — tool name to async handler mapping
world.add_component(agent, ToolRegistryComponent(tools=tools, handlers=handlers))The sandboxed_execute function runs tool handlers with timeout and output size limits.
from ecs_agent.tools import sandboxed_execute
result = await sandboxed_execute(
func=my_handler,
args={"expression": "2 + 2"},
timeout=30.0, # Maximum execution time in seconds
max_output_size=10000 # Maximum output size in bytes
)from ecs_agent.components import SandboxConfigComponent
world.add_component(agent, SandboxConfigComponent(timeout=10.0, max_output_size=5000))The ToolApprovalSystem provides human-in-the-loop approval for tool calls before execution.
from ecs_agent.types import ApprovalPolicy
# Three policies available:
ApprovalPolicy.ALWAYS_APPROVE # All tool calls pass through
ApprovalPolicy.ALWAYS_DENY # All tool calls are blocked
ApprovalPolicy.REQUIRE_APPROVAL # Human must approve each callfrom ecs_agent.components import ToolApprovalComponent
from ecs_agent.systems.tool_approval import ToolApprovalSystem
world.add_component(agent, ToolApprovalComponent(
policy=ApprovalPolicy.REQUIRE_APPROVAL,
timeout=60.0, # Seconds to wait for approval; None for infinite wait
))
world.register_system(ToolApprovalSystem(priority=-5), priority=-5)- LLM generates tool calls (via
ReasoningSystem). ToolApprovalSystemintercepts pending calls beforeToolExecutionSystem.- For
REQUIRE_APPROVALpolicy:- A
ToolApprovalRequestedEventis published with aFuture[bool]. - External code resolves the future:
event.future.set_result(True)to approve,Falseto deny. - On timeout (or
ToolTimeoutError), the call is denied.
- A
- Approved calls proceed to
ToolExecutionSystem; denied calls are removed.
ToolApprovalRequestedEvent(entity_id, tool_call, future)— Requesting approvalToolApprovedEvent(entity_id, tool_call_id)— Call was approvedToolDeniedEvent(entity_id, tool_call_id)— Call was denied
The approval system must run before tool execution:
ToolApprovalSystemat priority-5ToolExecutionSystemat priority5
See examples/tool_approval_agent.py for a full working demo.