# AI can't see a clock. So how did it tell me the time?

URL: https://systemdesignsimple.com/how-ai-tells-the-time-with-no-clock/
Published: 2026-11-11
Updated: 2026-11-12
Category: AI Systems
Publisher: System Design Simple (https://systemdesignsimple.com)

## Summary

A model cannot read a clock: it only sees the tokens in its request. The app offers a tool, the model writes a structured request to call it, the app runs it and sends the result back, and the model answers from that text. In our scripted toy demo, guesses without a tool were off by a median of 522.7 days; with the tool, 20 of 20 runs were within a minute, for a simulated 484 ms extra.

## Key facts

- **Where the time comes from:** The app's code reads the clock inside the tool. The model only reads the tool result as text in its next request
- **No tool (scripted toy model):** 11 of 20 scripted refusals, 9 of 20 scripted guesses off by a median of 752,645 minutes (522.7 days), a value set by the assumed guess date 2025-06-01
- **With the tool (scripted toy model):** 20 of 20 runs called it and landed within 1 minute of the virtual clock; median error 29.1 s, because the reply rounds down to the minute
- **Cost of the tool path (simulated):** Median 484 ms extra per run: tool 2 ms plus follow-up model call 482 ms. Almost all of it is the second model round trip
- **Rule of thumb:** Model calls = tool steps + 1. Each tool step adds one full model round trip, and the follow-up request resends the whole conversation
- **What the lab is not:** A scripted, seeded function with assumed refusal, skip and timezone-slip rates and simulated latency. It shows the mechanism, not how any real model behaves

A language model is a function from input tokens to output tokens: it has no clock, no network and no file system, so it cannot know what time it is unless something puts the time into its input. Tool calling is the mechanism that lets the model ask the surrounding app to run code, such as reading a clock, and get the result back as text. Interview version: "How does an LLM app answer questions that need live data, and what does the extra step cost?"

## Prerequisites {#prerequisites}

**What one model request contains.** Every call to a chat model is stateless: the app sends the instructions, the conversation so far and any extra data, and nothing persists inside the model between calls. The [context window article (AI 3)](/my-chat-forgot-my-name/) shows what the app resends each turn. This article adds two more kinds of content to that request: tool definitions and tool results.

**Tokens and generation.** A model writes its output one token at a time; the [streaming article (AI 2)](/why-ai-starts-answering-before-done/) explains that loop and its timing. A tool call is a reply whose tokens form a structured request instead of prose.

**Remote calls.** When one program asks another to run a function and return the result, that is a remote procedure call (RPC): a name, arguments, a result or an error, and a network trip in between. Part 47 of the series covers RPC and gRPC in depth.

## Definitions {#definitions}

> Definition: **Tool**: a function the app will run on the model's behalf, described by a name, a plain-language description and an argument schema. Some APIs call it a function.

> Definition: **Tool call**: structured model output naming one tool and its JSON arguments, instead of a text reply. The model writes it; the app runs it.

> Definition: **Tool result**: the message the app sends back after running a tool, tied to the call by an id, holding the output or an error as text.

> Definition: **JSON Schema**: a standard vocabulary for describing the shape of JSON data (types, required fields, patterns). Tool APIs use it for arguments.

> Definition: **Agent loop**: app code that calls the model, runs the tools it requests, appends the results and repeats until a final reply or a limit.

> Definition: **Training cutoff**: the latest date of a model's training data. It is fixed when training ends and says nothing about the date of use.

> Definition: **IANA timezone**: a named region such as `Asia/Tokyo` from the IANA Time Zone Database, which records its UTC offset and daylight saving rules over time.

## Why can a model not know the time? {#why-no-clock}

A model's output is computed from two things only: its weights, fixed when training ended, and the tokens in the current request. No hidden register holds the time. If the request does not say what "now" is, the model cannot compute it.

The weights do give a rough sense of the period the training data came from: the training cutoff. But a model is released some time after its cutoff and then used for a long time, so the distance between "the last thing it read about" and "today" is unknown to it and grows every day.

[Image: Whiteboard sketch of a model card with a frozen calendar page on an old date, a timeline beneath it with a you are here pin far to the right, and an arrow showing the gap between them | What the model learned stops on a past date; the gap to today is invisible from inside.]

So without help, a model asked "What time is it in Tokyo?" can refuse, guess, or use something in its input. Our lab scripts the first two on purpose. In mode A of <a href="https://github.com/systemdesignsimple/labs/tree/main/labs/ai-5"><code>labs/ai-5</code></a>, a scripted toy model with no tool refused in 11 of 20 runs and guessed in 9, always a random time on a fixed date, 2025-06-01, written into the code as an assumption. Against a virtual clock at 2026-11-05 23:00 Tokyo time, the median guess was off by 752,645 minutes = 752,645 / 1,440 = 522.7 days. That number follows entirely from the assumed date; it is not a measurement of any real model. Still, with nothing in the input, being right is luck.

There are two ways to put "now" into the input:

1. **The app writes it into the prompt.** App code reads the server clock and adds <code>Current date: 2026-11-05 (Thursday). User timezone: Asia/Tokyo.</code> to the system prompt. Zero extra round trips.
2. **The app offers a tool.** The model asks for the time, with arguments, when it needs it. One extra round trip per use, but exact at that moment and for any timezone.

Either way, app code reads the clock. The rest of this article covers the second, which generalises to all live data and actions.

## How it works: one tool call, step by step {#how-it-works}

Tool APIs use different field names, but the shape is the same (compare the [Anthropic tool use guide](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) and the [OpenAI function calling guide](https://developers.openai.com/api/docs/guides/function-calling)). One complete exchange for the Tokyo question:

**Step 1: the app sends the question and the tool list.** Each tool has a name, a description written for the model, and a JSON Schema for its arguments:

```json
{
  "name": "get_current_time",
  "description": "Returns the current local time for an IANA timezone such as Asia/Tokyo.",
  "input_schema": {
    "type": "object",
    "properties": {
      "timezone": { "type": "string", "description": "IANA timezone name" }
    },
    "required": ["timezone"],
    "additionalProperties": false
  }
}
```

The provider renders these definitions into the model's input in a format the model was trained on. So definitions cost input tokens on every request, and the description is effectively prompt text: a vague description produces vague tool use.

**Step 2: the model emits a tool call instead of a reply.** The model was trained to produce a structured block when a tool would help. The API parses it and returns it as data, with a stop reason meaning "I want a tool" (<code>stop_reason: "tool_use"</code> in Anthropic's Messages API, <code>finish_reason: "tool_calls"</code> in OpenAI's Chat Completions API). The call carries a unique id, the tool name and the arguments:

```json
{ "type": "tool_use", "id": "call_01", "name": "get_current_time", "input": { "timezone": "Asia/Tokyo" } }
```

Some APIs give the arguments as an object, others as a JSON string. Either way they are generated tokens that can be malformed or wrong: untrusted input to your system.

[Image: Whiteboard sketch of a your app server box between a phone and a model card, a tool shelf with get_time, search and calculator cards, and a sticky note reading get_current_time Asia/Tokyo sliding from the model to the app | The model writes a note naming a tool and its arguments; the app decides what to do with it.]

**Step 3: the app checks and runs the tool.** Ordinary application code looks up the name, checks permissions, validates the arguments, runs the function with a timeout and captures the output or error. In the lab, this is the only place the clock is read:

```text
app runs tool  [sim 1 ms] {"timezone":"Asia/Tokyo","local":"2026-11-05 23:10:15",
                           "utc_offset":"+09:00","utc_iso":"2026-11-05T14:10:15.459Z"}
```

**Step 4: the app sends a new request with the result.** It appends the model's tool-call message as returned, plus a tool result message referencing the same id (<code>tool_use_id</code>, <code>tool_call_id</code> or <code>call_id</code>, depending on the API), and calls the model again. The model is stateless, so this follow-up carries everything: instructions, tool definitions, question, tool call and result.

**Step 5: the model writes the final reply from the result.** The time is now in its input as text: "It's 23:10 on 2026-11-05 in Tokyo right now."

In the lab code the scripted model function receives only <code>messages</code> and never touches <code>clock.now</code>. The model asks, the app's code does the work, the answer returns as text. "The AI used a tool" means exactly that exchange.

## What does the app check before it runs a tool? {#before-running}

The model's output decides which function runs with which arguments, so a tool call is an entry point into your system, like an HTTP request from the internet. Anything that influences the model's output (the user, a web page it read, a document it summarised) influences the call. So the app checks, in order:

1. **Is the tool known?** Look the name up in a fixed registry. Never map a model-written string to an arbitrary function or shell command.
2. **Is this caller allowed to use it?** An allow-list per user, session or feature. Filter the tools you offer, and check again at execution time, because the model can name a tool that was not offered.
3. **Are the arguments valid?** Parse the JSON and validate against the schema: types, required fields, no unknown fields, patterns, ranges. Then validate meaning, which a schema cannot express: is <code>Asia/Tokyo</code> a real IANA zone, is this path inside the user's folder, does this account belong to this user.
4. **Does it need a human?** Read-only tools such as a clock can run automatically. Side effects (email, deleting files, moving money) need explicit confirmation showing the exact arguments, and idempotency so a repeat does no extra harm.
5. **Within limits?** Per-tool rate limits, a timeout per call, and a cap on result size, since the result returns to the context window as paid input tokens.

Schema-constrained generation, where offered, removes malformed JSON, not wrong values or unauthorised intent.

## What is the agent loop, and when does it stop? {#agent-loop}

Step 4 is a loop in disguise: after a result, the model may ask for another tool (look up the user's city, then the time there). The app continues until the model replies without a tool call:

```text
messages = [system, user]
for step in 1..MAX_STEPS:
    reply = model(messages, tools)
    messages += reply
    if reply has no tool calls: return reply.text
    for each call in reply.tool_calls (in parallel when independent):
        result = check_and_run(call)          # errors become results too
        messages += tool_result(call.id, result)
stop: "gave up after MAX_STEPS model calls"
```

[Image: Whiteboard sketch where the app lifts the get_time card, a clock card shows the real time, an arrow carries the time back to the model card, and three arrows close into a circle labelled loop | Ask, run, return, answer, repeated until the model stops asking: the agent.]

Nothing in the model guarantees it stops asking, so the loop needs explicit stop conditions:

- **Maximum steps.** A hard cap on model calls per request. Without it, a model that keeps requesting a tool (or a tool that keeps failing) burns tokens until something else breaks.
- **Budget.** A cap on tokens, tool time or wall-clock time. Each turn resends the growing conversation, so total input tokens across one request grow roughly with the square of the number of turns when results are large.
- **Repeat detection.** The same tool with the same arguments twice in a row, after a success, signals a problem: send a short note to the model or stop.
- **A clear end state.** On a limit, return a defined "could not finish" outcome and log the full trace.

Most APIs also offer a tool choice setting (auto, force a tool, forbid tools). Leaving the choice to the model is what makes the loop an agent rather than a script.

## What does a tool call cost in latency? {#latency}

A tool step adds one full model round trip, not just the tool's own time. For a request with <em>k</em> sequential tool steps:

```text
model calls  = k + 1
total time  ≈ Σ (model call i)  +  Σ (tool time in step j)

with parallel calls inside one step:
tool time of a step = max(tool times), not the sum
```

The lab records this inside each tool-using run rather than subtracting medians of separate samples. All of it is simulated with assumed ranges: first model call 400 to 1,000 ms, follow-up 300 to 800 ms, tool 1 to 5 ms. Across 20 tool-mode runs the median added cost was 484 simulated ms: median tool time 2 ms, median follow-up call 482 ms. The tool is almost free; the second model call is the cost.

[Image: Lab terminal card printing with tool 20/20 within 1 min, median error 29.1 s, added about 484 ms simulated, next to a grid of 20 green dots | Scripted toy model, simulated latency: every tool run was within a minute, and the price was one extra model round trip.]

The follow-up call is also heavier in tokens: it carries everything the first did plus the call and the result. So a tool returning a 50 KB JSON document when the model needs one field is a latency and cost bug. As the [latency vs throughput article](/ten-lanes-still-slow/) shows, more parallel capacity does not shorten a chain of dependent steps.

Ways to reduce the cost:

1. **No tool for stable facts.** Today's date and the user's timezone change once a day; put them in the system prompt. Place them after the large stable part (instructions, tool definitions), at day granularity, so provider-side prefix caching, where offered, still reuses the stable prefix. A timestamp with seconds at the top makes every prefix unique.
2. **Parallel tool calls.** Tokyo and London times can be requested in one turn and run concurrently: one step instead of two, saving a model round trip.
3. **Cache tool results** that are safe to reuse briefly (an exchange rate for 60 seconds). Never cache the clock or side effects.
4. **Shrink tool results** to the fields the model needs.
5. **Stream the final reply** (see the [streaming article](/why-ai-starts-answering-before-done/)) and show "checking the time" during the tool step.

## Why a tool call is just RPC {#rpc}

Strip away the model and the exchange is familiar: a function name and serialized arguments go to something that runs the function elsewhere and returns a result or an error. That is a remote procedure call. The only new part is that a model decides when to call and with what arguments.

[Image: Whiteboard sketch of the whole system with an orange handwritten bracket around the app, the tool shelf and the clock card labelled RPC, and a badge reading Part 47, Call a function on another computer | Underneath, a tool call is a function running somewhere else. The model only picks when to call it.]

The mapping is close enough that RPC lessons transfer directly:

| RPC concept (gRPC terms) | Tool calling equivalent |
|---|---|
| Service definition in an IDL (<code>.proto</code>) | Tool definition with a JSON Schema |
| Method name + serialized request message | Tool name + JSON arguments |
| Request id / correlation id | Tool call id matched by the tool result |
| Deadline on the call | Per-tool timeout set by the app |
| Status code + error details | Error returned as a tool result (some APIs have an <code>is_error</code> flag) |
| Retries with backoff | The model, or the app, calling again after an error |
| Idempotent methods | Read-only tools are safe to repeat; side-effect tools need idempotency keys |

**Deadlines.** gRPC asks every caller to set a deadline, because a call without one can wait forever holding resources (see the gRPC deadlines guide). A fetch tool hanging for 60 seconds stalls the whole agent loop for 60 seconds. Give each tool a timeout and the request an overall budget.

**Errors as results.** An RPC client turns an error into an exception. In an agent loop it is usually better to return the error to the model in plain words ("unknown timezone Mars/Olympus_Mons; use an IANA name like Asia/Tokyo"), so it can fix the argument, try another tool or explain. Keep errors the model cannot fix (authentication failures, an exhausted budget) on the app side.

**Idempotency and retries.** A model may repeat a call after a timeout, and the app may retry a step. For <code>get_current_time</code> that is harmless; for <code>send_payment</code> it is a double charge. Treat side-effecting tools as non-idempotent RPCs: one idempotency key per intended action (the tool call id is a natural one), and reject a second execution with the same key.

**Availability.** Every tool is a dependency in series with the model: a request needing both succeeds only when both are up, the product rule from [two servers in a row](/two-servers-in-a-row/).

## Worked example {#worked-example}

The video's question, "What time is it right now in Tokyo?", runs 20 times without tools and 20 times with <code>get_current_time</code>. All behaviour comes from a scripted toy model with assumed rates (refuse 45% without a tool, skip the tool 5%, timezone slip 5%), a virtual clock from 2026-11-05 23:00 Tokyo time, and simulated latency.

```text
Mode A, no tool (scripted)
  refusals                          11 / 20
  guesses                            9 / 20, all on the assumed date 2025-06-01
  median guess error                 752,645 min / 1,440 = 522.7 days
                                     (2025-06-01 to 2026-11-05 is 522 days, plus the time-of-day gap)

Mode B, with get_current_time (scripted)
  called the tool                   20 / 20
  within 1 minute of virtual clock  20 / 20
  median error                       29.1 s
    why not 0: the reply says "23:10", rounding 23:10:15 down to the minute,
    and the virtual clock moves on during the follow-up call, so the error
    is (seconds dropped) + (follow-up time), somewhere between 0 and about 61 s

Added cost per tool run (simulated, median of per-run values)
  tool                               2 ms
  follow-up model call             482 ms
  total added                      484 ms   (median of the per-run sums)
```

The printed trace: the first model call (992 simulated ms) returns <code>get_current_time({"timezone":"Asia/Tokyo"})</code>, the app runs it in 1 simulated ms and gets <code>2026-11-05 23:10:15</code>, and the follow-up call (585 simulated ms) writes "It's 23:10 on 2026-11-05 in Tokyo right now."

The assumed 5% timezone slip did not fire in these 20 runs with this seed. Had it fired, the scripted model would have read the <code>utc_iso</code> field (14:10) as Tokyo time: an error of exactly 9 hours = 540 minutes, since Tokyo is UTC+9 all year.

## Code {#code}

A complete agent loop in one file: a tool registry with JSON Schema arguments, an allow-list, a small validator, per-tool timeouts, parallel calls, errors returned as results and a maximum-steps guard. The model is a fake that reads only messages, so no API key is needed. Node.js 18 or newer, no packages.

```js
// agent_loop.mjs: a tool-calling agent loop with a fake model. Node 18+, no packages, no network.
// The fake model only reads messages. Only the app's tool code reads the clock.

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const MODEL_MS = 150;        // pretend one model round trip takes 150 ms (an assumption)
const TOOL_TIMEOUT_MS = 500; // the app gives each tool call at most 500 ms
const MAX_STEPS = 4;         // the app stops the loop after 4 model calls

// 1. Tool definitions: a name, a description and a JSON Schema for the arguments.
const TOOLS = {
  get_current_time: {
    description: 'Current local date and time for one IANA timezone, e.g. Asia/Tokyo.',
    parameters: {
      type: 'object',
      properties: { timezone: { type: 'string', pattern: '^[A-Za-z_]+(/[A-Za-z_]+)+$' } },
      required: ['timezone'],
      additionalProperties: false,
    },
    run: async ({ timezone }) => {
      const now = new Date();               // the only line in this file that reads a clock
      const local = new Intl.DateTimeFormat('sv-SE', {
        timeZone: timezone, dateStyle: 'short', timeStyle: 'medium',
      }).format(now);                       // throws RangeError for an unknown timezone
      return { timezone, local, utc_iso: now.toISOString() };
    },
  },
  delete_file: {
    description: 'Deletes a file.',
    parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
    run: async () => ({ deleted: true }),   // never reached: not on the allow-list
  },
};
const ALLOWED = new Set(['get_current_time']); // what this user may run, checked by the app

// 2. A tiny validator for the schema subset used above.
function validate(schema, args) {
  if (typeof args !== 'object' || args === null) return 'arguments must be an object';
  for (const k of schema.required ?? []) if (!(k in args)) return `missing ${k}`;
  for (const [k, v] of Object.entries(args)) {
    const p = schema.properties?.[k];
    if (!p) { if (schema.additionalProperties === false) return `unknown field ${k}`; continue; }
    if (p.type === 'string' && typeof v !== 'string') return `${k} must be a string`;
    if (p.pattern && !new RegExp(p.pattern).test(v)) return `${k} does not match ${p.pattern}`;
  }
  return null;
}

// 3. The fake model: it sees messages and tool names, never the clock.
let nextId = 1;
async function fakeModel(messages, toolNames) {
  await sleep(MODEL_MS);
  const user = messages[0].content;
  const results = messages.filter((m) => m.role === 'tool').map((m) => JSON.parse(m.content));
  const call = (name, args) => ({ id: `call_${nextId++}`, name, arguments: JSON.stringify(args) });
  if (user.includes('loop forever')) return { role: 'assistant', tool_calls: [call('get_current_time', { timezone: 'Asia/Tokyo' })] };
  if (results.length === 0) {
    if (user.includes('delete')) return { role: 'assistant', tool_calls: [call('delete_file', { path: '/tmp/x' })] };
    const zones = [...user.matchAll(/[A-Z][a-z]+\/[A-Za-z_]+/g)].map((m) => m[0]);
    if (zones.length && toolNames.includes('get_current_time')) {
      return { role: 'assistant', tool_calls: zones.map((tz) => call('get_current_time', { timezone: tz })) };
    }
    return { role: 'assistant', content: 'I have no clock, so I cannot know the current time.' };
  }
  const lines = results.map((r) => (r.error ? `error: ${r.error}` : `${r.timezone} ${r.local.slice(11, 16)}`));
  return { role: 'assistant', content: lines.join(' · ') };
}

// 4. The app runs one tool call: allow-list, parse, validate, run with a timeout.
async function runTool(tc) {
  const tool = TOOLS[tc.name];
  if (!tool) return { error: `unknown tool ${tc.name}` };
  if (!ALLOWED.has(tc.name)) return { error: `tool ${tc.name} is not allowed here` };
  let args;
  try { args = JSON.parse(tc.arguments); } catch { return { error: 'arguments are not valid JSON' }; }
  const bad = validate(tool.parameters, args);
  if (bad) return { error: `invalid arguments: ${bad}` };
  const timeout = new Promise((r) => setTimeout(() => r({ error: 'tool timed out' }), TOOL_TIMEOUT_MS));
  try { return await Promise.race([tool.run(args), timeout]); }
  catch (e) { return { error: `${e.name}: ${e.message}` }; } // errors go back to the model as results
}

// 5. The agent loop.
async function agent(prompt) {
  const t0 = performance.now();
  const ms = () => `${Math.round(performance.now() - t0)} ms`.padStart(7);
  const messages = [{ role: 'user', content: prompt }];
  console.log(`\n> ${prompt}`);
  for (let step = 1; step <= MAX_STEPS; step++) {
    const out = await fakeModel(messages, Object.keys(TOOLS));
    messages.push(out);
    if (!out.tool_calls) { console.log(`${ms()}  reply: ${out.content}  (${step} model calls)`); return; }
    for (const tc of out.tool_calls) console.log(`${ms()}  model asks: ${tc.name}(${tc.arguments})`);
    const results = await Promise.all(out.tool_calls.map(runTool)); // parallel tool calls
    out.tool_calls.forEach((tc, i) => {
      console.log(`${ms()}  app returns: ${JSON.stringify(results[i])}`);
      messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(results[i]) });
    });
  }
  console.log(`${ms()}  stopped: ${MAX_STEPS} model calls without a final reply`);
}

await agent('What time is it?');
await agent('What time is it in Asia/Tokyo?');
await agent('Time in Asia/Tokyo and Europe/London?');
await agent('What time is it in Mars/Olympus_Mons?');
await agent('Please delete my temp file.');
await agent('Asia/Tokyo, loop forever');
```

```sh
node agent_loop.mjs
```

What to observe (timings from one run on our Mac; the 150 ms model delay is an assumption):

- **"What time is it?"** No timezone, so the fake model answers from nothing: one model call, about 150 ms.
- **"...in Asia/Tokyo?"** Two model calls, about 300 ms. The tool takes about a millisecond; the second model call is the cost. The <code>local</code> and <code>utc_iso</code> fields differ by exactly 9 hours.
- **"...Tokyo and Europe/London?"** Two tool calls in one step via <code>Promise.all</code>: still about 300 ms, not 450 ms.
- **"Mars/Olympus_Mons"** passes the schema pattern but <code>Intl.DateTimeFormat</code> throws <code>RangeError</code>, which returns as a tool result. Schema and semantic validation are different layers.
- **"delete my temp file"** <code>delete_file</code> exists but is not on the allow-list; the app refuses before running anything.
- **"loop forever"** The loop stops after 4 model calls, about 600 ms, with a defined message.

The article's numbers come from our lab script (<a href="https://github.com/systemdesignsimple/labs/blob/main/labs/ai-5/run.mjs"><code>labs/ai-5/run.mjs</code></a> in our <a href="https://github.com/systemdesignsimple/labs/tree/main/labs/ai-5">public lab repository</a>). It is seeded on a virtual clock, so every run prints the same traces, summary rows and <code>RESULT</code> line; the inputs are listed in the worked example above.

## Failure modes and edge cases {#failure-modes}

**Timezone slip.** The tool returns a UTC timestamp and a local time, and the model reports the wrong one as local (an assumed 5% in the lab; plausible whenever a result has similar-looking fields). Detect it with tests that compare the reply's hour with the tool's local field. Fix it at the source: return only what is needed, name fields unambiguously (<code>local_time_tokyo</code>, not <code>time</code>), include the offset (RFC 3339, <code>2026-11-05T23:10:15+09:00</code>), or return a ready-to-say string.

**Daylight saving and fixed offsets.** A hard-coded +9 works for Tokyo all year and fails for <code>Europe/London</code>, which switches between UTC+0 and UTC+1. Convert with the IANA database (<code>Intl</code>, the OS zoneinfo or a library), never a stored offset, and resolve ambiguous input such as "IST", which names more than one zone.

**Stale date in a long session.** A date injected once at session start is wrong after midnight. Inject it per request.

**The model skips the tool** and answers from its weights (an assumed 5% in the lab). If an answer must come from a tool, force tool choice for that step, or check in code that the reply's facts appear in a tool result.

**Invented tools and fields.** The registry lookup and <code>additionalProperties: false</code> catch them; log them, since a rising rate often means a confusing description.

**Hanging tools.** Per-tool timeouts and an overall deadline keep the loop moving. <code>Promise.race</code> in the example stops waiting but does not cancel the work; real tools should accept an <code>AbortSignal</code>.

**Runaway loops.** Repeated or oscillating calls multiply cost. Max steps, a token budget, repeat detection and a per-request trace (each call, arguments, timings) make them visible.

**Untrusted text steering tools.** A tool result such as a web page can contain instructions the model follows next turn. Treat results as data, keep permissions tied to the user, and confirm side effects. The next part of the series is about this failure.

## Trade-offs {#trade-offs}

| Option | Good for | Costs you | Use when |
| --- | --- | --- | --- |
| Date and timezone in the system prompt | Zero extra round trips, every answer knows "today" | A few tokens per request; stale within a long session if injected once; can break prefix caching if placed early with seconds | Date-level questions, scheduling hints, "this week" reasoning |
| A get_current_time tool | Exact time at the moment of use, any timezone | One extra model round trip per use (484 ms median in our simulation), more tokens in the follow-up | Exact times, other cities, long sessions |
| Both | Cheap default with exact fallback | Two sources that can disagree if the prompt date is stale | General assistants |
| Parallel tool calls | Independent lookups in one step | More concurrent load on tools; harder ordering and error handling | Several independent reads in one question |
| Fixed workflow (app calls tools, no model choice) | Predictable, testable, minimal latency | No flexibility; new questions need new code | A known sequence, such as always fetching the account before answering |
| Open agent loop | Handles questions you did not plan for | Variable latency and cost, needs limits, permissions and tracing | Open-ended tasks with many possible tools |

## Common mistakes {#common-mistakes}

- **Mistake:** believing the model "looked up" the time. **Instead:** trace where the value entered the request: the system prompt or a tool result. If neither, it was a guess.

- **Mistake:** running whatever function name the model writes. **Instead:** a fixed registry, an allow-list checked at execution time, and schema plus semantic validation of every argument.

- **Mistake:** throwing an exception out of the loop when a tool fails. **Instead:** return a clear error as the tool result so the model can correct itself.

- **Mistake:** an agent loop with no step limit. **Instead:** cap model calls, tokens and wall-clock time per request.

- **Mistake:** returning UTC and local time side by side with vague names. **Instead:** return the value to say, with its offset, under an unambiguous name.

- **Mistake:** adding a tool round trip for a fact that changes once a day. **Instead:** put today's date and the user's timezone in the prompt.

## In an interview {#in-an-interview}

1. **Clarify:** what live data or actions are needed, read-only or with side effects, and what may this user do?
2. **Assumptions:** a model round trip and a tool time (say 600 ms and 20 ms) and tool steps per question.
3. **Structure:** client, app server running the agent loop, model API, tool services; mark where permissions, validation and timeouts sit.
4. **Deep dive:** schema definitions, tool call with an id, app-side checks and execution, tool result, follow-up call; model calls = steps + 1; stop conditions.
5. **Trade-offs:** prompt-injected facts vs tools, parallel calls, caching, fixed workflow vs open loop, confirmation for side effects.
6. **Wrap up:** metrics (steps per request, tool error rate, p95 per step, limit hits) and a trace per request.

> **A 60-second answer**A model only computes over the tokens in its request, so it cannot read a clock or a database. The app gives it tools, each with a name, a description and a JSON Schema for its arguments. When a tool would help, the model emits a tool call with an id, a name and JSON arguments. The app looks the tool up in a fixed registry, checks the user may use it, validates the arguments, runs it with a timeout and returns the output or error as a tool result tied to that id. Then it calls the model again with the whole conversation, until the model replies without a tool call or a step limit is hit. That loop is the agent. Each tool step costs a full model round trip, so I put stable facts like today's date in the prompt, allow parallel calls, keep results small, and treat side-effecting tools like non-idempotent RPCs with confirmation and idempotency keys.

## Practice {#practice}

**Q (Beginner): Where does the model's knowledge of the current time come from in a tool-calling app?**
From the app's code, which reads a clock before the request (date in the prompt) or inside a requested tool. The model reads the result as text.

**Q (Beginner): What is the difference between a training cutoff and today's date?**
The cutoff is where training data ends, fixed in the weights. Today's date can be months or years later and is unknown unless the request contains it.

**Q (Intermediate): A question needs 3 sequential tool steps. How many model calls happen, and what dominates latency?**
4 model calls (steps + 1). With tools that take milliseconds, the model round trips dominate: in our simulation one tool step added a median 484 ms, of which the tool was 2 ms.

**Q (Intermediate): The user asks for the time in Tokyo and London. How do you avoid two extra round trips?**
Parallel tool calls: both requested in one turn and run concurrently, so one tool step and one follow-up call.

**Q (Intermediate): The model passes timezone "Mars/Olympus_Mons", which matches your schema pattern. What happens and what should happen?**
The schema accepts it; the IANA lookup fails. Return a clear error as the tool result ("unknown timezone, use a name like Asia/Tokyo") so the model can correct itself.

**Q (Advanced): A tool returns utc_iso and local fields, and replies sometimes give Tokyo time 9 hours early. Diagnose and fix.**
The model is reading the UTC field as local time (the lab's timezone slip; Tokyo is UTC+9, so the error is exactly 540 minutes). Return only the local value with its offset under an unambiguous name, and test the reply's hour against the result.

**Q (Advanced): Why should a send_payment tool be treated like a non-idempotent RPC?**
A repeat after a timeout would pay twice. Use an idempotency key per intended action (such as the tool call id), reject duplicates, and require confirmation.

**Q (Advanced): Where do you put today's date in the prompt, and why does position matter?**
After the large stable prefix (instructions, tool definitions), at day granularity. Provider-side prefix caching, where offered, reuses identical prompt prefixes; a timestamp with seconds at the top makes every prefix unique and defeats it.

## Cheat sheet {#cheat-sheet}

| Term or formula | Meaning |
| --- | --- |
| Tool | A function the app will run for the model: name, description, JSON Schema for arguments |
| Tool call | Model output naming a tool with JSON arguments and an id; the app runs it |
| Tool result | Output or error sent back to the model, tied to the call id |
| Agent loop | Call model, run requested tools, append results, repeat until a reply or a limit |
| Model calls = k + 1 | k sequential tool steps need one more model call than steps |
| Step time = max(parallel tools) | Parallel calls in one step cost the slowest tool, not the sum |
| Training cutoff | Where the model's training data ends; not today's date |
| IANA timezone | Named zone such as Asia/Tokyo with full offset and daylight saving history |
| RFC 3339 timestamp | `2026-11-05T23:10:15+09:00`: date, time and offset in one string |
| Before running a tool | Known tool, allowed for this caller, valid arguments, confirmation if side effects, within limits |
| Our lab (scripted, simulated) | No tool: 11/20 refusals, guesses off 522.7 days (assumed date). Tool: 20/20 within 1 min, median 29.1 s, +484 ms |

## Further reading {#further-reading}

- **Anthropic, Tool use with Claude:** tool definitions, tool use and tool result blocks, tool choice and parallel tool use, with request examples.
- **OpenAI, Function calling:** the same flow in another API, including tool call ids, strict schema mode and several calls per turn.
- **JSON Schema:** the schema language for tool arguments, including <code>required</code>, <code>pattern</code> and <code>additionalProperties</code>.
- **gRPC, Introduction:** service definitions, stubs and the RPC model tool calling resembles.
- **gRPC, Deadlines:** why every remote call needs a deadline; the same reasoning applies to tool timeouts.
- **IANA Time Zone Database:** zone names and daylight saving rules used by operating systems and <code>Intl</code>.
- **RFC 3339:** the timestamp format with an explicit offset.

Next in the series: a web page hides one sentence, and the AI obeys it. For the queueing side of tool latency, see [why a server is fast alone and slow when crowded](/fast-alone-slow-crowded/).

This article describes general patterns for tool calling and agent loops, illustrated with our own small scripted demonstration. The lab numbers come from a scripted toy model with assumed behaviour rates, a virtual clock and simulated latency in one Node.js process. They are not measurements of any company's model, product or private system.

## Frequently asked questions

**How does an AI chatbot know the current time?**
It does not know it on its own. Either the app writes the current date and time into the request (often in the system prompt), or the app offers a tool such as get_current_time that the model can ask for. In both cases the app's code reads a real clock and the model reads the result as text.

**What is a tool call in an AI app?**
A tool call is a structured request the model writes instead of a normal reply: the name of a function the app has described, plus arguments as JSON. The app checks and runs the function, then sends the result back to the model in a new request so it can write the final answer.

**Does the model run the tool itself?**
No. The model only produces text. The app parses the tool-call request, decides whether it is allowed, validates the arguments, runs its own code and returns the result. Nothing runs unless the app's code runs it.

**What is an AI agent?**
In system terms, an agent is a loop in the app: call the model, run any tools it asks for, append the results, and call the model again until it gives a final reply or a limit such as a maximum number of steps is reached.

**Should I put the date in the system prompt or use a tool?**
Put stable, cheap facts such as today's date and the user's timezone in the prompt, because it costs no extra round trip. Use a tool when the answer must be exact to the second, changes during a long session, or depends on an argument such as a different city.

**Were these numbers measured on a real AI model?**
No. They come from a scripted toy model in labs/ai-5 (seed 20261105, 20 runs per mode) with assumed behaviour rates, a virtual clock and simulated latency. They illustrate the mechanism and the shape of the cost, not the behaviour of any product.

## Sources

- [Anthropic documentation: Tool use with Claude](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)
- [OpenAI documentation: Function calling](https://developers.openai.com/api/docs/guides/function-calling)
- [JSON Schema: official site and specification](https://json-schema.org/)
- [gRPC documentation: Introduction to gRPC](https://grpc.io/docs/what-is-grpc/introduction/)
- [gRPC documentation: Deadlines](https://grpc.io/docs/guides/deadlines/)
- [IANA: Time Zone Database](https://www.iana.org/time-zones)
- [RFC 3339: Date and Time on the Internet: Timestamps](https://www.rfc-editor.org/rfc/rfc3339)
