C.W.K.
Stream
Lesson 03 of 04 · published

Multi-Turn Tool Loop

~16 min · agentic, tool-loop, multi-turn

Level 0Spark
0 XP0/35 lessons0/10 achievements
0/140 XP to next level140 XP to go0% complete

The agentic loop in 12 lines

An "agent" is a tool loop. Call the model, check for tool calls, execute them, append results, call again. Stop when the model returns text instead of more calls. That's it.

The shape of one iteration

  1. Send contents to the model.
  2. If response has no function_calls, return response.text — done.
  3. Otherwise: append the model's turn to contents (so it remembers what it called).
  4. Execute every function call (in parallel if independent).
  5. Append a single user turn containing all the function-response parts.
  6. Loop.

Parallel calls are normal

Modern Gemini models often emit multiple function_call parts in one turn. Execute them concurrently and return all results in one user turn — the API uses the id field to match results to calls.

You need a budget

Tool loops can run forever if the model keeps re-calling. Always bound iteration count and total elapsed time.

Code

The minimal loop·python
from google import genai
from google.genai import types

client = genai.Client()

def agent_loop(initial_prompt, tools, dispatch, max_iterations=10):
    contents = [types.Content(
        role='user',
        parts=[types.Part(text=initial_prompt)],
    )]
    config = types.GenerateContentConfig(tools=[tools])

    for iteration in range(max_iterations):
        response = client.models.generate_content(
            model='gemini-2.5-flash',
            contents=contents,
            config=config,
        )
        calls = response.function_calls or []
        if not calls:
            return response.text

        # Append the model's call turn (so it remembers)
        contents.append(response.candidates[0].content)

        # Execute calls and build a user-turn of results
        result_parts = []
        for fc in calls:
            result = dispatch(fc.name, dict(fc.args))
            result_parts.append(types.Part.from_function_response(
                name=fc.name,
                response={'result': result},
                id=fc.id,
            ))
        contents.append(types.Content(role='user', parts=result_parts))

    raise RuntimeError(f'Loop did not terminate within {max_iterations} iterations')
Parallel execution with asyncio·python
import asyncio

async def agent_loop_async(prompt, tools, dispatch_async, max_iter=10):
    contents = [types.Content(role='user', parts=[types.Part(text=prompt)])]
    config = types.GenerateContentConfig(tools=[tools])

    for _ in range(max_iter):
        response = await client.aio.models.generate_content(
            model='gemini-2.5-flash', contents=contents, config=config,
        )
        calls = response.function_calls or []
        if not calls:
            return response.text

        contents.append(response.candidates[0].content)

        # Run all calls concurrently
        results = await asyncio.gather(*[
            dispatch_async(fc.name, dict(fc.args)) for fc in calls
        ])

        contents.append(types.Content(role='user', parts=[
            types.Part.from_function_response(
                name=fc.name, response={'result': r}, id=fc.id,
            )
            for fc, r in zip(calls, results)
        ]))

    raise RuntimeError('Loop budget exhausted')
Dispatch table — wire tool name to Python function·python
TOOLS_REGISTRY = {
    'set_light_values': lambda args: actually_set_lights(**args),
    'get_weather':      lambda args: weather_api(args['location']),
    'lookup_order':     lambda args: db.get_order(args['order_id']),
}

def dispatch(name, args):
    fn = TOOLS_REGISTRY.get(name)
    if not fn:
        return {'error': f'Unknown tool: {name}'}
    try:
        return fn(args)
    except Exception as e:
        # Return errors to the model — let it decide whether to retry
        return {'error': str(e)}

# Now call it
final_text = agent_loop(
    'Set the lights warm and 30%, then check the weather in Seoul.',
    tools=Tool(function_declarations=[set_lights, get_weather]),
    dispatch=dispatch,
)

External links

Exercise

Build a 3-tool agent: get_current_time, add_numbers, concat_strings. Wire them into agent_loop from the first code block. Ask Flash: "What is 47 + 53? Then concatenate the result with ' is the answer'. Then tell me what time it is." Watch it call all three tools across multiple turns. Confirm max_iterations kicks in if you sabotage one of the tools to always return errors.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.