Skip to Content
AgentDeck 6.0 is here: serve one deck over HTTP, AG-UI or the terminal.See what's new

Tools

Tools allow agents to perform actions and fetch external information.

Defining a Tool

@tool declares a leaf capability. The body is your own Python, sync or async, and the docstring is what the model reads. A sync body runs on a bounded worker pool the deck owns, never on the event loop; see Context: what each context can do for what that changes about ctx.safepoint().

import asyncio from agentdeck import Agent, Deck, tool @tool async def lookup_order(order_id: str) -> str: """Fetch status for an order.""" return f"Order {order_id} is in transit." agent = Agent(name="order_bot", instructions="Look up customer orders.", tools=[lookup_order]) deck = Deck(agents=[agent]) async def main() -> None: async with deck: result = await deck.run("order_bot", "where is order A-1003?") print(result.output) asyncio.run(main())

The plain function

A context-free callable in tools= is compiled the same way, with less ceremony:

def lookup_order(order_id: str) -> str: """Fetch status for an order.""" return f"Order {order_id} is in transit."

Both forms get the same schema and the same failure attribution. What @tool adds is a catalog entry: it yields a NativeDefinition the deck holds, so a workflow can start it as a child run of its own with ctx.invoke(lookup_order, "A-1003"). A plain function is not a target ctx.invoke can resolve.

@toolplain function
Called by a modelyesyes
Sync or async bodyyesyes
ToolCtx[T] injectedyesno - build() refuses it
Started as its own child runyesno
Name and descriptionname=, description=, else the function’sthe function’s

A callable that declares ToolCtx[T] has to be @tool. The parameter is injected by the runtime and must stay off the schema the model sees, so @tool is what makes “this callable receives a live dependency” part of its own declaration rather than something a reader has to notice from the annotation alone. A plain function carrying one fails build(), naming the fix:

lookup_order takes a ToolCtx parameter ('shop') but is not declared @tool. Only @tool carries a context into a tool - add the decorator: ...

You already have SDK tools

A tool already decorated with function_tool from the OpenAI Agents SDK (agents) is accepted in tools= and passed through untouched. That passthrough has three consequences:

  • It cannot take a ToolCtx parameter. The decorator puts every parameter into the schema the model sees, and a context has none, so it raises PydanticSchemaGenerationError at the decorator line.
  • It keeps whatever failure_error_function it was given, so its exceptions never reach tool.call.completed.error.
  • It is engine-native and carries no portability guarantee.

Reach for it to adopt tools you already have. Write new ones with @tool.

Handling Failures

A tool can return a user-facing failure like any other result. Do that when the model can recover and continue the conversation:

@tool async def lookup_shift(date: str) -> str: """Return the nurse on duty for a date.""" row = SHIFTS.get(date) if row is None: return f"no shift on record for {date}" return row

If a tool raises instead, agentdeck records the call as tool.call.completed with the exception type and message on its error field, and hands the model the Agents SDK’s standard tool-error string. The run is not failed, so it can still complete: Deck.run() returns the model’s final answer, the non-streamed HTTP route returns 200 {"output": ...}, and the streamed HTTP route ends with done if the model completes.

An exception that escapes the SDK runner is a different outcome, and a pre-decorated function_tool whose own failure handling re-raises is the way to get one. Then the log records run.failed with error_code="engine_error", Deck.run() raises the exception, Deck.stream() yields run.failed and then raises, the non-streamed HTTP route returns 500 {"detail": "internal error"}, and the streamed HTTP route sends an error event with the exception type.

Tools do not carry images

A tool returns text. Returning a data: URI, a file path or base64 from a tool does not let the model see an image: it sees the string. Images travel as content blocks on the way in, not as tool results on the way back. See sending an agent an image.

Last updated on