Skip to content

p5: Agentic: the model orchestrates

The first half of this track was workflows you wrote. You fixed the steps, you wrote the if/else, and the model filled in the words. Now the model takes the wheel. TripMate becomes a real agent here.

You hand it three tools and one request. It looks up who you are, checks the weather where you’re going, prices the flight from your home city, then writes the pitch. That order is a real dependency chain: it can’t check the weather before it has a destination, or price a flight before it knows your home city. The model reads the tool descriptions and works the order out itself, with no orchestration code from you.

In a hurry? These three steps are the whole challenge. Everything below is the why and the how.

  1. Run npm run p5 and count the [tool fired] lines: only two fire (lookupTraveler, getWeather), so no flight price is grounded.
  2. Edit start/agent.ts: write the getFlights tool around the ready FLIGHTS table (TODO 2), and wire it into the agent’s tools map (TODO 2).
  3. Done when three [tool fired] lines run in dependency order and a real £142 price comes from your tool, with steps: 4.

lookupTraveler also arrives here. In f1 to f5 the agent knew nothing about you. This tool fetches your profile (name, home city, interests, budget) from the app, so the agent can greet you by name instead of guessing.

"plan my trip"  ->  TripMate sequences the tools itself:
     lookupTraveler  ->  getWeather  ->  getFlights  ->  compose  ->  answer

You write no if/else; the model decides which tool to call and in what order.

Forget travel. Say a billing agent has three tools and one request, “look into my last charge”:

const agent = new ToolLoopAgent({
  model,
  tools: { lookupAccount, getLatestInvoice, checkRefund },
  stopWhen: stepCountIs(8),
  instructions: `
You handle billing questions.
Call lookupAccount for the customer id, getLatestInvoice for that customer, then checkRefund for that invoice.
Never invent amounts; only state what a tool returned.
`.trim(),
});

The instructions describe the job, not the order. There is no “first do this, then that” in your code: getLatestInvoice needs a customer id and checkRefund needs an invoice id, so the model has to call them in that order. It reads each tool’s description, sees what each one needs, and derives the sequence itself. stopWhen: stepCountIs(8) is the step budget: each tool call plus the final reply is a step, so a chain needs room for every link. Below you give TripMate its third tool and watch the same thing happen.

Open start/agent.ts. The whole world is in that one file: three inlined data tables, with lookupTraveler and getWeather already wired into the agent, whose instructions describe the job and not the order. What is missing is getFlights: that tool is yours to write (TODO 2), using the two tools above as templates.

npm run p5

The starter wires lookupTraveler and getWeather, but not getFlights yet. TripMate greets you as Jag and describes Lisbon’s weather. You’ll see two [tool fired] lines, so no flight price is grounded. If it states a flight price anyway, that price is invented; if it hedges or refuses, that’s useful signal too. The point is that nothing is pricing the flight yet, and you’re about to write the tool that does.

  1. Run it and count the tool calls. Run npm run p5 and watch for the [tool fired] lines. There are two: lookupTraveler and getWeather. Read the pitch and notice any flight detail has nothing behind it.

  2. Write the getFlights tool (TODO, step 2). Build it from the two tools already in the file, don’t copy one from here. getWeather is your closest template: it declares a description the model routes on, an inputSchema, and an execute that logs a [tool fired] line, looks a value up in a table, and returns it (or an { error } when the table has no matching row). getFlights is the same shape over the FLIGHTS table instead of WEATHER: it takes a from and a to, builds the route key the table uses, and returns the matching flight. The TODO comment in start/agent.ts names each piece; write the tool there against the ready FLIGHTS data.

  3. Wire it into the tools map (TODO, step 2). The agent can only call tools listed in its tools map, so add getFlights alongside lookupTraveler and getWeather. Run again. Now three [tool fired] lines run in dependency order: lookupTraveler, then getWeather(Lisbon), then getFlights(London -> Lisbon). The £142 price comes from your tool, and steps climbs to 4: three tool calls plus the compose.

  4. Starve the loop and watch it break (TODO, step 3). Set stopWhen to stepCountIs(1), predict what you’ll get, then run:

    stopWhen: stepCountIs(1),

    The agent makes one tool call and the loop stops before it can chain the rest, so the pitch comes out half-built. Raise it to 2, then back to 8, and watch the [tool fired] lines return one at a time. A dependency chain needs as many steps as it has links, plus one to compose. Put it back to 8 when you’re done.

  5. Check you’ve got it. You should see three [tool fired] lines in order, a real £142 price, and steps: 4. Scroll up to the console trace and find the same chain drawn as a tree. You should be able to say why the model called the tools in that order, and why the destination is pinned to Lisbon in the prompt.

Stuck? finish/agent.ts is the canonical version. Read it after you’ve had a real go.

  • Only two [tool fired] lines after step 3. You wrote getFlights but didn’t add it to the tools map. The agent can only call tools that are in the map.

  • getFlights is not defined or a type error. Check your tool against the two templates above it. tsx runs without type-checking, so a missing comma or brace is the usual cause.

  • The pitch names a different city than the tool calls. Small models drift when the workload is heavy. The destination is pinned to Lisbon in the prompt to keep the chain crisp; a stronger model can both pick the destination and orchestrate.

  • A flight call returns an error. The inlined FLIGHTS table only has London to Lisbon. Keep the prompt on Lisbon, or add rows to the table.

Why not just write the order myself?

You could: call lookup, then weather, then flights, in your own code. That works until the questions vary. “Is it warm enough to hike in Lisbon?” needs weather but not flights. “How much to get to Barcelona?” needs flights but not weather. Writing the branching for every shape of question is the routing logic you’re trying to avoid.

The agent reads each request and calls only the tools that request needs, in the order their results require. You maintain tools, not a decision tree.

Where is the "context" injected?

lookupTraveler returns the traveller profile from its execute, which closes over the inlined TRAVELER data. In a real app that execute would read the authenticated session or a database.

The model never sees how the data arrives. It sees a tool that returns the user’s profile. The Python path does the same thing through its first-class RunContext/deps; the AI SDK uses a closure. Both reach the same outcome.

Next up is p6, where this agent’s tools become other agents and it orchestrates them.