Query helpers as tools
Subagents do not write SQL directly. They call small, well-named helper functions that take a few typed arguments and return serializable dicts. This keeps the graph code clean and lets you unit test the database layer without spinning up a graph.
def get_customer(customer_id: str) -> dict | None:
with _conn() as con:
row = con.execute(
'SELECT * FROM customers WHERE customer_id = ?', (customer_id,)
).fetchone()
if row is None:
return None
record = dict(row)
record['claims'] = list_claims(customer_id)
return record
def list_claims(customer_id: str) -> list[dict]:
with _conn() as con:
rows = con.execute(
'SELECT * FROM claims WHERE customer_id = ? ORDER BY opened_at DESC',
(customer_id,),
).fetchall()
return [dict(r) for r in rows]get_customer returns the customer row with their claim history nested inside. The billing subagent gets everything it needs from one function call. The claims subagent uses list_claims directly when it does not need customer identity.
from datetime import date
def open_claim(customer_id: str, claim_type: str, amount: float) -> dict:
"""Open a new claim in 'review' status and return it."""
with _conn() as con:
row = con.execute('SELECT COUNT(*) AS n FROM claims').fetchone()
claim_id = f'CLM-{5000 + row["n"] + 1}'
record = {
'claim_id': claim_id,
'customer_id': customer_id,
'type': claim_type,
'status': 'review',
'amount': float(amount),
'opened_at': date.today().isoformat(),
}
con.execute(
'INSERT INTO claims (claim_id, customer_id, type, status, amount, opened_at) '
'VALUES (:claim_id, :customer_id, :type, :status, :amount, :opened_at)',
record,
)
return recordNew claims always enter in 'review' status. The subagent never gets to mark a claim approved or paid. That is a safety rule: the AI can file a claim on behalf of the user, but approval goes through a human.
The @tool decorator is useful when you want the LLM to choose which function to call from a list. In the supervisor pattern, the LLM does not choose. The supervisor picks the subagent and the subagent decides which helpers to call from code, not from a prompt. That means we get faster, cheaper, more deterministic behavior at the cost of giving up one layer of LLM reasoning we do not actually need here.
Matching exercise: Match each helper to the subagent that uses it
Loading practice…
Checkpoint: Data foundations checkpoint
Loading practice…