GAP·MAP

AI agents and tool use

[ junior depth ]

A tool call is a request to your application

A language model cannot run your refundOrder function merely by naming it. Your request describes the available tool, including its name, purpose, and argument schema. The model may return a structured function call. Application code decides whether to execute it and sends the result back.

Wrong "Function calling gives the model direct access to my functions."

Right "The model proposes a function name and arguments. My application owns validation, authorization, execution, and error handling."

As of July 2026, an OpenAI Responses API function definition uses this top-level shape:

const tools = [{
  type: "function",
  name: "lookup_order",
  description: "Fetch an order by its public order ID.",
  parameters: {
    type: "object",
    properties: {
      orderId: { type: "string" },
    },
    required: ["orderId"],
    additionalProperties: false,
  },
  strict: true,
}];

The model returns JSON-encoded arguments. Treat them as untrusted input even when strict schema mode is enabled. Strict mode constrains the generated structure. Your business rules still decide whether the caller may read that order.

The loop around the model

An agent runner repeatedly calls the model and inspects what came back. In the OpenAI Agents SDK, text of the required output type ends the run only when the response has no tool calls. A function call sends control to application code; its result becomes a new observation for the model. A handoff can switch the active agent in SDKs that support that feature.

People often summarize this as plan, act, observe. The visible API contract is more concrete:

  1. Send the current input and available tools to the model.
  2. Inspect every returned item because one response may contain more than one function call.
  3. Validate and execute allowed calls.
  4. Return each result with the matching call identifier.
  5. Call the model again, or stop because a configured limit or policy says to stop.

In the Responses API, a function result is tied to the request with call_id:

input.push({
  type: "function_call_output",
  call_id: item.call_id,
  output: JSON.stringify({ status: "found", order }),
});

Wrong "The tool result is only for logging because the model already knows what the function did."

Right "The result is the model's observation. It must be returned if the model needs it to choose another action or write the final answer."

Stop conditions belong to the runner

Tool use can fail, or the model can keep choosing tools without finishing. A runner needs a turn limit, cancellation, and an error policy. The OpenAI Agents SDK runner defaults maxTurns to 10 as of July 2026 and throws MaxTurnsExceededError when that limit is reached.

A loop is useful when later steps depend on observations that code could not enumerate up front. If the path is already known, ordinary control flow stays easier to predict.

[ middle depth ]

Fixed workflows and agent-controlled routing

A workflow can use models and tools without giving a model control over the route. This module follows Anthropic's architectural distinction: code chooses a fixed workflow's path, while a model dynamically directs an agent's next tool or step from the observations so far.

Wrong "Any system with several model calls is an agent."

Right "The control path is the distinction. Code owns a workflow's path; the model dynamically directs an agent's path."

Keep a known sequence in code. This version makes the policy boundary visible and testable:

const extracted = await extractInvoice(document);
const checked = checkPolicy(extracted, policy);

if (checked.kind === "approved") {
  await postInvoice(checked.invoice);
} else {
  await routeToExceptionQueue(checked);
}

An agent fits the exception branch when the missing information and number of searches vary by case. It can inspect the failure, choose a read tool, observe the result, and revise its next action. The loop still needs an explicit outcome such as resolved, needs-human, or failed.

Orchestration choices change who owns the answer

Manager-style orchestration keeps one agent responsible for the final response and exposes specialists as tools. A handoff changes which agent owns the current conversation. Code orchestration can classify an input, chain specialists, run independent work concurrently, or repeat generation and evaluation until a bounded condition is met.

Use code when the dependency graph is known. Independent calls can be run concurrently and combined after both finish:

const [policyResult, fraudResult] = await Promise.all([
  runPolicyCheck(report),
  runFraudCheck(report),
]);

return combineChecks(policyResult, fraudResult);

Only parallelize independent calls. If one call needs an identifier returned by another, run them sequentially. OpenAI's function-calling API may return multiple function calls in one model turn. Setting parallel_tool_calls: false limits that turn to zero or one tool call.

Memory is supplied context plus stored state

The model does not gain durable application memory because the runner uses a loop. Something must store prior items and supply the relevant context on a later turn.

As of July 2026, the OpenAI Agents SDK documents four conversation-state strategies: pass result.history, use an SDK session, use an OpenAI conversationId, or continue with previousResponseId. The first two are client-managed. The last two are OpenAI-managed Responses strategies. Mixing full client history with server-managed state can duplicate context.

Wrong "Conversation memory is a database that the model keeps accurate."

Right "Conversation history helps the next model call. Durable business facts still need an application-owned source of truth."

Separate working context from authoritative records:

const caseRecord = await cases.load(caseId); // authoritative status and approvals
const session = await sessions.open(caseId); // model conversation items

await run(agent, "Continue this case", {
  session,
  context: { caseId, currentStatus: caseRecord.status },
});

Stored history also creates retention and privacy work. The SDK session interface permits custom storage and history editing, so the application can apply its own retention, encryption, and metadata policies. Trimming or summarizing history changes what evidence the next model call can see, so evaluate those policies against real tasks.

Errors are observations only when the loop can use them

Returning a tool error to the model can let it choose a different input or tool. Retrying the same state-changing action can apply it again. Classify failures, cap attempts, and reconcile the current business state before retrying a write.

if (error instanceof ValidationError) {
  return { ok: false, retryable: false, reason: error.message };
}

if (attempt >= MAX_ATTEMPTS) {
  throw new Error("tool retry limit reached");
}

The useful loop is observable and stoppable. Trace the model calls, selected tools, arguments, results, handoffs, approvals, and terminal reason.

[ senior depth ]

Autonomy spends a reliability budget

Agents help when the route cannot be enumerated before the task starts. Open-ended search, repository changes across an unknown set of files, and exception handling can benefit from choosing actions after each observation. Fixed workflows remain a better fit when the steps and validation gates are known.

Anthropic's official guidance describes the trade: agentic systems often pay more latency and cost for better task performance, while autonomy introduces compounding-error risk. OpenAI's orchestration guide likewise says code orchestration is more deterministic and predictable for speed, cost, and performance.

Wrong "More agents make a system more capable, so every specialist should become an autonomous agent."

Right "Add model-controlled routing only where evaluations show that a fixed path falls short."

This boundary keeps deterministic checks in code and gives an agent a narrow exception task:

const decision = evaluateKnownRules(report);

if (decision.kind === "complete") {
  return persistDecision(decision);
}

return run(exceptionAgent, decision.missingEvidence, {
  maxTurns: 6,
  context: { reportId: report.id },
});

An evaluator loop also needs a bounded stop condition. "Repeat until the evaluator is happy" can consume turns without a measurable improvement.

Guardrails have boundaries

Input, output, and tool guardrails cover different points. In the OpenAI Agents SDK as of July 2026, input guardrails run for the first agent in a chain, output guardrails run for the agent producing the final output, and function-tool guardrails run around every invocation of the function tool on which they are configured. Agent-level input and output checks therefore do not imply per-tool enforcement throughout a manager or handoff workflow.

Place the policy at the action boundary:

const sendEmail = tool({
  name: "send_email",
  description: "Send one reviewed email to an allowlisted domain.",
  parameters: emailSchema,
  needsApproval: true,
  inputGuardrails: [allowlistedRecipientGuardrail],
  execute: deliverEmail,
});

The SDK's approval flow pauses before a configured tool executes and returns interruptions that the application can approve or reject before resuming the same run state. Approval is useful for consequential actions, but approval fatigue is a real design constraint. Narrow credentials and deterministic policy checks reduce how much judgment each prompt asks from a person.

Wrong "Strict JSON Schema makes a tool safe."

Right "Strict mode constrains generated arguments. Authorization, least privilege, business invariants, and approval constrain effects."

Prompt injection crosses the observation boundary

Search results, email, files, and tool output can contain instructions written by an attacker. Anthropic describes prompt injections as adversarial instructions hidden in content a model processes and warns that no browser agent is immune.

Treat retrieved content as data and restrict the actions available after reading it. The enforcement belongs outside the content itself:

const permissions = {
  readMailbox: true,
  sendEmail: false,
};

if (toolName === "send_email" && !permissions.sendEmail) {
  return { ok: false, reason: "sending is disabled for this run" };
}

Relevant controls include a small tool surface, narrow credentials, argument validation, approval for consequential writes, isolation, and audit logs. These controls limit consequences; they do not turn untrusted text into trusted instructions.

Failure modes span several layers

The model can select the wrong tool, produce invalid arguments, or keep looping. Tools can time out or return errors. Memory strategies can duplicate context when mixed, retain sensitive history, or omit evidence after trimming. A trace is needed when orchestration makes the decision path hard to reconstruct.

Test the trajectory as well as the final prose. Anthropic's agent-evaluation guidance describes agents as multi-turn systems that call tools, modify state, and adapt to intermediate results. Useful evaluations therefore record task completion and the path taken: tool choice, argument validity, policy violations, retries, side effects, turns, and terminal reason.

For a function tool, bound how long one invocation may run. The OpenAI Agents SDK supports timeoutMs and can return a timeout to the model or raise ToolTimeoutError:

const lookupPolicy = tool({
  name: "lookup_policy",
  parameters: policyQuerySchema,
  timeoutMs: 5_000,
  timeoutBehavior: "raise_exception",
  execute: fetchPolicy,
});

Memory, retries, and guardrails do not repair a vague success condition. An agent needs an observable environment, a verifiable target, and a point where control returns to code or a person.

dig deeper

Primary sources behind these notes - the specs and official docs worth reading in full.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more AI Engineering

was this useful?