Agents become useful when they can do things beyond generating text. Tools let them call functions, query databases, make API requests—anything you can express in code.
How It Works
Define a function with type hints and a docstring. Pass it to runner.run(). The Dedalus SDK extracts the schema automatically and handles execution when the model decides to use it.
import asyncio
from dedalus_labs import AsyncDedalus, DedalusRunner
from dotenv import load_dotenv
load_dotenv()
def as_bullets(items: list[str]) -> str:
"""Format items as a bulleted list."""
return "\n".join(f"• {item}" for item in items)
async def main():
client = AsyncDedalus()
runner = DedalusRunner(client)
result = await runner.run(
input=(
"Take the following events and call as_bullets with a list of items (one per event).\n\n"
"Events:\n"
"- Warriors vs Lakers — San Francisco — 2026-01-18\n"
"- Warriors vs Suns — San Francisco — 2026-01-22\n"
"- Warriors vs Celtics — San Francisco — 2026-01-29\n\n"
"Return only the list."
),
model="openai/gpt-5.2",
tools=[as_bullets],
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
import Dedalus from 'dedalus-labs';
import { DedalusRunner } from 'dedalus-labs';
const client = new Dedalus();
const runner = new DedalusRunner(client, true);
function formatTable(rows: Record<string, any>[]): string {
if (!rows.length) return 'No results.';
const cols = Object.keys(rows[0]);
const header = `| ${cols.join(' | ')} |`;
const sep = `| ${cols.map(() => '---').join(' | ')} |`;
const body = rows.map((r) => `| ${cols.map((c) => String(r?.[c] ?? '')).join(' | ')} |`);
return [header, sep, ...body].join('\n');
}
async function main() {
const result = await runner.run({
input:
'Take the following events and call formatTable with a list of rows (one row per event).\n\n' +
'Events:\n' +
'- {"name":"Warriors vs Lakers","city":"San Francisco","date":"2026-01-18"}\n' +
'- {"name":"Warriors vs Suns","city":"San Francisco","date":"2026-01-22"}\n' +
'- {"name":"Warriors vs Celtics","city":"San Francisco","date":"2026-01-29"}\n\n' +
'Return only the table.',
model: 'openai/gpt-5.2',
tools: [formatTable],
});
console.log(result.finalOutput);
}
main();
The model sees the tool schemas, decides which to call, and the Runner executes them. Multi-step reasoning happens automatically—the Runner keeps calling the model until it can complete the task.
Good tools typically have:
- Type hints on all parameters and return values
- Docstrings that explain what the tool does (the model reads these)
- Clear names that indicate purpose
# Good: typed, documented, clear name
def get_weather(city: str, units: str = "celsius") -> dict:
"""Get current weather for a city. Returns temperature and conditions."""
return {"temp": 22, "conditions": "sunny"}
# Bad: no types, no docs, unclear name
def do_thing(x):
return some_api_call(x)
// Good: typed, documented, clear name
function getWeather(city: string, units: string = 'celsius'): object {
// Get current weather for a city
return { temp: 22, conditions: 'sunny' };
}
// Bad: no types, unclear name
function doThing(x: any) {
return someApiCall(x);
}
Tools can be async. The Runner awaits them automatically:
async def fetch_user(user_id: int) -> dict:
"""Fetch user profile from database."""
async with db.connection() as conn:
return await conn.fetchone("SELECT * FROM users WHERE id = $1", user_id)
async function fetchUser(userId: number): Promise<object> {
// Fetch user profile from database
const result = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
return result.rows[0];
}
Wrap a specialized agent as a tool. The coordinator delegates specific tasks to specialists without giving up conversation control.
This differs from handoffs:
- Handoffs: New agent takes over the conversation with full history
- Agent as tool: Specialist receives specific input, returns output, coordinator continues
import asyncio
from dedalus_labs import AsyncDedalus, DedalusRunner
async def main():
client = AsyncDedalus()
runner = DedalusRunner(client)
# Specialist: wrap another runner call as a tool
async def research_specialist(query: str) -> str:
"""Deep research on a topic. Use for questions requiring thorough analysis."""
result = await runner.run(
input=query,
model="openai/gpt-5.2", # Stronger model for research
instructions="You are a research analyst. Be thorough and cite sources.",
mcp_servers=["tsion/exa"] # Web search via Exa
)
return result.final_output
async def code_specialist(spec: str) -> str:
"""Generate production code from specifications."""
result = await runner.run(
input=spec,
model="anthropic/claude-opus-4-5", # Strong at code
instructions="Write clean, tested, production-ready code."
)
return result.final_output
# Coordinator: cheap model that delegates to specialists
result = await runner.run(
input="Research quantum computing breakthroughs in 2025, then write a Python simulator for a basic quantum gate",
model="openai/gpt-4o-mini",
tools=[research_specialist, code_specialist]
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
import Dedalus from 'dedalus-labs';
import { DedalusRunner } from 'dedalus-labs';
const client = new Dedalus();
const runner = new DedalusRunner(client);
// Specialist functions
async function researchSpecialist(query: string): Promise<string> {
const result = await runner.run({
input: query,
model: 'openai/gpt-4o',
instructions: 'You are a research analyst. Be thorough.',
mcpServers: ['tsion/exa'], // Web search via Exa
});
return result.finalOutput;
}
async function codeSpecialist(spec: string): Promise<string> {
const result = await runner.run({
input: spec,
model: 'anthropic/claude-opus-4-5',
instructions: 'Write clean, production-ready code.',
});
return result.finalOutput;
}
// Coordinator delegates to specialists
const result = await runner.run({
input: 'Research AI trends, then write a TypeScript example',
model: 'openai/gpt-5.2',
tools: [researchSpecialist, codeSpecialist],
});
When to use this pattern:
| Scenario | Why Agent-as-Tool |
|---|
| Vision/OCR tasks | Text-only coordinator delegates images to vision model |
| Code generation | Fast model triages, strong model writes code |
| Domain specialists | Generic router -> specialized instructions/model |
| Cost optimization | Cheap coordinator, expensive specialists only when needed |
Model Selection
Tool calling quality varies by model. For reliable multi-step tool use:
openai/gpt-5.2 and openai/gpt-4.1 handle complex tool chains well. Older or smaller models may
struggle with multi-step reasoning.
Next steps
- Route across models: Handoffs — Route to different models mid-conversation
- Return typed data: Structured Outputs — Validate and parse JSON into schemas
- Stream progress: Streaming — Show tool-call progress in real time
- See full examples: Use Cases — End-to-end agent patterns