Compound Tools¶
CompoundTool implements the agent decides, system acts pattern. An agent produces structured output, an executor runs a tool or action using that output, and state is optionally updated with the result. Hooks fire around the execution step.
The Pattern¶
Basic Usage¶
import asyncio
from orqest.agents import BaseAgent, GlobalState, CompoundTool
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
query: str = Field(description="Search query")
max_results: int = Field(default=5, description="Number of results")
class SearchAgent(BaseAgent[GlobalState, SearchQuery]):
async def _run_implementation(self, state: GlobalState, **kwargs) -> SearchQuery:
user_message = state.get_latest_message("user")
result = await self.call_model(user_message, state)
return result.output
async def execute_search(query: SearchQuery, state: GlobalState):
"""The executor — runs the actual search."""
return [f"Result {i} for: {query.query}" for i in range(query.max_results)]
async def main():
agent = SearchAgent(
agent_name="search_planner",
system_prompt="Generate a search query from the user's request.",
output_type=SearchQuery,
model="openai:gpt-4.1",
api_key="sk-...",
)
tool = CompoundTool(
agent=agent,
executor=execute_search,
)
state = GlobalState()
# run() injects `prompt` into state as a user message before the agent runs.
agent_output, results = await tool.run(state, prompt="Find papers on quantum error correction")
print(f"Query: {agent_output.query}")
print(f"Results: {results}")
asyncio.run(main())
Hook Integration¶
Hooks fire around the executor step (not the agent call):
from orqest.hooks import HookRunner
class AuditHook:
async def before_tool(self, tool_name, args, state):
print(f"[AUDIT] {tool_name} starting with {args['prompt']}")
async def after_tool(self, tool_name, args, result, state, duration_ms):
print(f"[AUDIT] {tool_name} completed in {duration_ms:.0f}ms")
async def on_error(self, tool_name, args, error, state):
print(f"[AUDIT] {tool_name} failed: {error}")
tool = CompoundTool(
agent=agent,
executor=execute_search,
hooks=HookRunner(hooks=[AuditHook()]),
name="search_tool", # used in hook dispatch
)
State Updater¶
An optional state_updater modifies state after execution:
def update_state(state: GlobalState, result) -> GlobalState:
state.add_message("assistant", f"Found {len(result)} results")
return state
tool = CompoundTool(
agent=agent,
executor=execute_search,
state_updater=update_state,
)
What's Happening Under the Hood¶
tool.run(state, prompt)injectspromptintostateas a user message, then callsagent.run(state)to get structured outputhooks.run_before(tool_name, args, state)dispatches to all hooksexecutor(agent_output, state)runs the action, timed forduration_ms- On success:
hooks.run_after(...)fires, thenstate_updater(state, result)if configured - On error:
hooks.run_error(...)fires, then the exception re-raises
The return value is a tuple: (agent_output, execution_result).
Related Concepts¶
- Hooks & Lifecycle -- the hook system that CompoundTool uses
- Agents -- the
BaseAgentthat produces structured output - Session Persistence -- persisting state across compound tool invocations