Build Stateful Agentic Apps with the Gemini Interactions API

63 min remaining
Step 5 of 12•6 min

5. Design Tools the Agent Can Reason With

If a tool description says "run this before that", the model follows instructions instead of reasoning. These tools are designed the other way round:

  • Describe capability and constraints, never order: Each description says what the tool does and what it needs. The agent works out the sequence.
  • Use typed arguments and enums: run_diagnostic(subsystem), vent_coolant(valve) and set_sector_power(sector, megawatts) give the model real choices, and enums stop it inventing values.
  • Offer cheap and expensive ways to know: get_station_status is broad but can be stale. run_diagnostic is accurate but covers one subsystem per action.
  • Return errors as data: A failed action comes back as a function_result with is_error set to True and a message that explains why, so the agent can change its plan instead of the script crashing.
  • Give it an exit: escalate_to_human lets the agent stop when there is no safe way forward, which is better than guessing.
Explicit JSON Schema Declarations

The Interactions API does not yet support automatic function calling from Python functions, so each tool is declared explicitly as a JSON Schema definition.

Add the code

Open tools.py, paste the following code and save the file:

tools.py(python)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
# ==========================================
# 1. Tool declarations (what the model sees)
# ==========================================
# Descriptions say what each tool does and what it needs.
# They never say in which order to use them: working that out is the agent's job.
 
TOOL_DECLARATIONS = [
{
"type": "function",
"name": "get_station_status",
"description": "Station-wide dashboard from the telemetry array. Cheap overview, but the array needs power to give live data.",
"parameters": {"type": "object", "properties": {}},
},
{
"type": "function",
"name": "run_diagnostic",
"description": "Reads one subsystem directly from its local sensors, on battery power. Accurate even when telemetry is down.",
"parameters": {
"type": "object",
"properties": {
"subsystem": {"type": "string", "enum": ["reactor", "power", "life_support", "comms"]},
},
"required": ["subsystem"],
},
},
{
"type": "function",
"name": "vent_coolant",
"description": "Releases coolant through one of two independent valves to lower the reactor core temperature. Coolant is limited.",
"parameters": {
"type": "object",
"properties": {
"valve": {"type": "string", "enum": ["A", "B"]},
},
"required": ["valve"],
},
},
{
"type": "function",
"name": "set_sector_power",
"description": (
"Sets the power allocation for one sector. Works while the bus is down (it changes the saved configuration). "
"Sectors: life_support (oxygen scrubbers), comms (telemetry array), "
"hydroponics (algae farm; the algae freeze if left unpowered for too long), research_labs (non-critical)."
),
"parameters": {
"type": "object",
"properties": {
"sector": {"type": "string", "enum": ["life_support", "comms", "hydroponics", "research_labs"]},
"megawatts": {"type": "integer", "minimum": 0, "maximum": 60},
},
"required": ["sector", "megawatts"],
},
},
{
"type": "function",
"name": "restart_power_bus",
"description": "Attempts to re-energise the main power bus using the saved sector configuration.",
"parameters": {"type": "object", "properties": {}},
},
{
"type": "function",
"name": "restart_life_support",
"description": "Attempts to start the oxygen scrubbers.",
"parameters": {"type": "object", "properties": {}},
},
{
"type": "function",
"name": "escalate_to_human",
"description": "Hands the incident to ground control. Use when there is no safe way to continue.",
"parameters": {
"type": "object",
"properties": {"reason": {"type": "string"}},
"required": ["reason"],
},
},
]
 
TOOL_NAMES = {tool["name"] for tool in TOOL_DECLARATIONS}
 
 
# ==========================================
# 2. Tool execution (what actually runs)
# ==========================================
 
def execute_tool(station, name, arguments):
"""Runs one tool against the station. Returns (output_text, is_error).
 
Errors are returned to the model instead of crashing the script,
so the agent can read what went wrong and adapt.
"""
if name not in TOOL_NAMES:
return f"Unknown tool '{name}'.", True
try:
output = getattr(station, name)(**(arguments or {}))
return output, output.startswith(("STATION LOST", "SESSION CLOSED"))
except (ValueError, RuntimeError, TypeError) as error:
return f"ERROR: {error}", True

How tools.py works

  • TOOL_DECLARATIONS: Defines the seven tools in the JSON Schema format expected by client.interactions.create(..., tools=TOOL_DECLARATIONS). Strict "enum" lists (["reactor", "power", "life_support", "comms"], ["A", "B"]) and numeric bounds ("minimum": 0, "maximum": 60) constrain the model's arguments at generation time.
  • TOOL_NAMES allowlist: Builds a set of valid tool names from TOOL_DECLARATIONS. execute_tool() checks if name not in TOOL_NAMES before calling getattr(station, name), ensuring the model can never invoke internal helper methods such as _advance_time or verdict.
  • Turning exceptions into observations (execute_tool): When a tool raises a ValueError, RuntimeError, or TypeError (for example, when Valve A is jammed or the power bus trips), execute_tool() catches the exception and returns (f"ERROR: {error}", True). The agent loop sends that string back to the model with "is_error": True so the model can adapt its plan instead of crashing the Python process.