The Tool Execution Loop
Introduction: The Tool Execution Loop
You've learned how to define tool schemas and parse function calls from model responses. Now you're ready to close the loop by actually executing those function calls and feeding their results back to the model. This lesson introduces Factor 3 (Own your context window) by showing you how to explicitly control what information flows through your agent's conversation history.
By the end of this lesson, you'll build agents that can perform multi-step reasoning, where the model calls a tool, your code executes it, and the model sees the result before deciding what to do next.
Defining Executable Functions
To execute function calls, you first need actual TypeScript functions that perform real work. These functions should be simple and focused, with clear inputs and outputs. For this lesson, you'll define two basic mathematical operations that your agent can use.
Each function uses TypeScript's type annotations to document what it expects (number) and returns. These are ordinary TypeScript functions that your application will call directly after the model requests them. They represent the actual capabilities your agent has — when the model decides to call add, your code will execute this function and return the result. Now you need to create tool schemas that tell the model these functions exist and how to use them.
Creating Tool Schemas for Your Functions
You'll create toolSchemas that match your TypeScript functions, plus a final_answer tool that signals when the agent has finished its work. The schemas tell the model what tools are available and exactly how to use them.
Notice how the schema for add matches the signature of your TypeScript function — the parameter names (a and b) are identical, and the types correspond. This consistency matters because when the model calls add with arguments, you'll parse those arguments from JSON and pass them directly to your function using switch-based dispatch. With your functions and schemas defined, you're ready to build the conversation context.
Initializing the Context with the User's Message
The context is an array that holds the entire conversation history between the user and the agent. You explicitly control what goes into this array, which implements Factor 3 by giving you full ownership of the context window.
Because the context can contain several structurally different item types — user messages, function calls, and function results — you define a ContextItem union type to describe all of them:
With that type in place, you initialize the context with just the user's opening message:
This typed array represents everything the model will see when you make your first API call. The user has asked the agent to perform two calculations, and the model will decide which tools to call based on this request and the available toolSchemas. Before making the call, you'll also need to define a systemPrompt that explains the agent's role.
Setting Up the System Prompt
You need a systemPrompt that explains the agent's role and instructs it to use tools when appropriate. Since you're providing explicit toolSchemas, the systemPrompt can remain simple and focused on behavior rather than output format.
The systemPrompt tells the agent when to use tools (for math) and when to signal completion (via final_answer). This gives the model clear behavioral guidance while the toolSchemas handle the structural requirements. With both the context and systemPrompt ready, you can make your first API call.
Making the First API Call
With the context initialized and the systemPrompt defined, you're ready to make the first API call. Create a client instance and call client.responses.create. Because this is an async operation, you use await:
The tool_choice: "required" parameter ensures that the model must call a tool rather than responding conversationally. For this two-turn walkthrough, the first request offers only the math tools; final_answer stays unavailable until we have processed the first set of calculations. Unit 4 will handle unrestricted tool selection inside a loop. This is important because you're building a structured agent that operates through tool calls, not free-form text. The model will analyze the user's request, recognize that it needs to perform two calculations, and call the appropriate tools. Now you need to process the response and identify those function calls.
Understanding the Context Message Structure
Before processing the response and building your context, you need to understand the exact structure the API expects for function-related messages. The ContextItem union type you defined earlier captures all three shapes directly.
For function call messages added to context, the API requires:
type: "function_call"— identifies this as a function call messagename: string— the name of the function being calledarguments: string— aJSONstring containing the function argumentscall_id: string— a unique identifier linking this call to its result
For function result messages added to context, the API requires:
type: "function_call_output"— identifies this as a function result messagecall_id: string— must match thecall_idfrom the original function calloutput: string— aJSONstring containing the function's result
The critical rule is that the call_id must match between a function call and its corresponding result. This linkage allows the API to understand which result belongs to which call, which becomes especially important when the model issues multiple function calls in a single response — as it may in this example. With this structure in mind, you're ready to process the response and build your context correctly.
Identifying and Recording the Function Call
After the API returns, you need to process the response by iterating through response.output and identifying function calls. The response.output array can contain different item types, so you use an early-continue guard to skip anything that isn't a function call. In TypeScript, this check also narrows the item's type, so everything inside the if-block is known to be a function call:
Recording each function_call in the context is crucial because the model needs to see its own decisions in the conversation history. The function call includes the tool name, the arguments as a JSON string, and a unique call_id that links the call to its eventual result. This call_id is generated by the API and serves as the tracking mechanism you'll use when adding the function's result back to the context. With the call recorded, you're ready to execute the actual TypeScript function.
Parsing Arguments and Dispatching to Functions
With the function call recorded in context, you parse the arguments and dispatch to the correct TypeScript function. Use JSON.parse(item.arguments) to convert the JSON string into an object, then assert its type so TypeScript knows the shape:
The type assertion as { a: number; b: number } is safe here because you set strict: true on the tool schemas — the model is required to produce arguments that match the declared shape. The switch statement dispatches to the right function and calls it with args.a and args.b directly. Unknown tool names throw an error rather than silently failing, which is good defensive programming.
Notice that this entire block runs inside a for loop over response.output. The model may issue several tool calls in a single response, and each one is processed in sequence. One possible response calls both add and multiply:
If both calls arrive in the first response, these executions produce the shown results. Now you need to add each result back to the context so the model can see what happened.
Recording the Function Result in Context
After executing each function, you push the result back into context. This is where the call_id becomes important — you use the same value to link this result to the original function call:
The output is formatted as a JSON string using JSON.stringify. The function_call_output type tells the API that this message contains the result of a previous function call, and the matching call_id links everything together. Because this push happens inside the same for loop, every function call executed in this pass gets its result recorded before you move on to the second API call.
If both calls arrive together, the context contains the original user message and the recorded function calls and results. A different model choice may require another tool turn; Unit 4 introduces the loop for that case.
Making the Second API Call with Complete History
Now you make a second API call with the updated context. The model will see the entire history: the user's request, every tool call it previously made, and the result of each execution. If all requested calculations were completed in the first response, it can call final_answer now. If it requests another calculation instead, this two-turn demonstration reports that no final answer is available; Unit 4 handles continued execution.
The same parameters are used as before, but now input contains the full conversation history, including both function executions. The model can use this context to understand that both calculations were successful and determine how to proceed. You'll process this second response to extract the final_answer.
Processing the Final Answer
You process the second response by finding the final_answer call in the output. Use .find() to locate it, verify its name, and parse its arguments to retrieve the answer string:
When both calculations arrive in the first response and the second returns final_answer, one possible result is:
The model used the full context — including both function calls and their results — to understand that all the requested calculations were complete and then provided both answers through the final_answer tool. This illustrates the request, tool execution, and result feedback. The next unit turns this two-turn walkthrough into a loop that can continue when the model requests more tools.
Summary: Owning Your Context Window
You've now implemented the complete tool execution pattern in TypeScript. The key steps were: defining TypeScript functions alongside their schemas, initializing a typed context array using the ContextItem union, iterating over response.output to find function calls, executing them with a switch-based dispatch and JSON.parse, pushing both the calls and their results back into context, and making a second API call where the model sees the full history before calling final_answer.
This pattern implements Factor 3 by giving you full control over the context window — you decide what information goes into each API call by explicitly managing the context array with the OpenAI TypeScript SDK, and you can inspect it at any time to understand exactly what the model sees. The loop-based processing also means the pattern naturally handles multiple tool calls in a single response, as you saw when both add and multiply were executed before the second API call. In the practice exercises, you'll implement this pattern yourself with different tools and scenarios.
