Build a ReAct agent
What you'll build: a finance research assistant that looks up stock news and today's date on its own, then answers in plain language — traced end-to-end, but calling your model provider directly (no gateway).
This tutorial uses the BYO (bring your own key) path: model calls go straight to your provider with your own key, while AcruxCore still stores the prompt, tool catalog, and traces. Two client tools — finance_research(ticker_symbol) and get_todays_date() — are catalogued in AcruxCore but run by your own code.
react_agent.ipynb
is this whole page as one runnable notebook, written for a first-timer: a preflight cell
that checks both keys and the Yahoo endpoint before anything else, both tools and the
prompt created step by step, the trace read back from the API, and four ways to get it
wrong triggered on purpose — including the quiet one, where skipping the llm span leaves
a trace with tool calls and no model turns. It reaches Yahoo Finance with requests
directly instead of through langchain-community, so it needs one dependency rather than
three. It renders on GitHub with its saved output, so you can read it through before
running anything.
A provider key for any OpenAI-compatible API, and for the Python tab, pip install requests.
Your code makes the completion call here, so the base URL is the only thing that decides who answers. Set the URL and the model together and nothing else on this page changes:
| Provider | PROVIDER_BASE_URL | A PROVIDER_MODEL it serves |
|---|---|---|
| OpenAI | https://api.openai.com/v1 | gpt-4o-mini |
| OpenRouter | https://openrouter.ai/api/v1 | meta-llama/llama-3.3-70b-instruct |
| Anthropic (its compatibility endpoint) | https://api.anthropic.com/v1 | claude-haiku-4-5-20251001 |
| a local server (vLLM, Ollama, LM Studio) | http://localhost:8000/v1 | whatever it loaded |
The examples below were run against OpenAI, so that is the default. Two things vary by provider: model ids are not shared, and each words its error bodies differently.
1. Create the tools over the API
A tool is a shell (name + description) plus an immutable version carrying the JSON schema the model reads. Both tools use "executor": {"type": "client"} — "my own code runs this," the same contract Build a tool-calling agent in Python (no SDK) uses for its get_weather tool.
You can also create tools through the dashboard (no code), or declare them in code with acrux.tool so the first run auto-registers them in the catalog.
- curl
- Python (SDK)
- Node (SDK)
# finance_research — takes a ticker symbol
curl -X POST "$ACRUXCORE_BASE_URL/tools" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"finance_research","description":"Search Yahoo Finance news for a ticker symbol."}'
{"id":"c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f","name":"finance_research","description":"Search Yahoo Finance news for a ticker symbol.","teamId":"73e9f801-9f43-412f-a359-4d23928b9eff","createdBy":"00cc9833-13ca-42b4-a102-b403e85ea250","createdAt":"2026-08-01T10:10:54.418Z"}
curl -X POST "$ACRUXCORE_BASE_URL/tools/c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f/versions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"description": "Search Yahoo Finance news for a ticker symbol.",
"parametersSchema": {"type":"object","properties":{"ticker_symbol":{"type":"string","description":"Stock ticker symbol, e.g. \"AAPL\"."}},"required":["ticker_symbol"]},
"executor": {"type":"client"}
}'
{"id":"50bd9670-cf0b-423e-a6bd-675c248a5c0f","toolId":"c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f","versionNumber":1,"description":"Search Yahoo Finance news for a ticker symbol.","source":"api","parametersSchema":{"type":"object","required":["ticker_symbol"],"properties":{"ticker_symbol":{"type":"string","description":"Stock ticker symbol, e.g. \"AAPL\"."}}},"executor":{"type":"client"},"createdAt":"2026-08-01T10:11:03.989Z","aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
get_todays_date takes no arguments at all — an empty properties object:
curl -X POST "$ACRUXCORE_BASE_URL/tools" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"get_todays_date","description":"Get today'"'"'s date."}'
curl -X POST "$ACRUXCORE_BASE_URL/tools/d0a800d5-a484-4d4f-a9e2-05a715775682/versions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"description": "Get today'"'"'s date.",
"parametersSchema": {"type":"object","properties":{}},
"executor": {"type":"client"}
}'
{"id":"ca59ea4a-d86b-45be-b424-263f97651f12","toolId":"d0a800d5-a484-4d4f-a9e2-05a715775682","versionNumber":1,"description":"Get today's date.","parametersSchema":{"type":"object","properties":{}},"executor":{"type":"client"},"aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
The @acrux.tool decorator derives the name, description, and JSON Schema from the function itself. tools.sync() reconciles the decorated function with the catalog — creating the tool if it doesn't exist, or committing a new version if the spec changed:
from acruxcore import AcruxCore, acrux
@acrux.tool
async def finance_research(ticker_symbol: str) -> str:
"""Search Yahoo Finance news for a ticker symbol.
Args:
ticker_symbol: Stock ticker symbol, e.g. "AAPL".
"""
# Your implementation here
...
@acrux.tool
async def get_todays_date() -> str:
"""Get today's date."""
return datetime.now().strftime("%Y-%m-%d")
async with AcruxCore() as hub:
results = await hub.tools.sync([finance_research, get_todays_date])
for fn, r in zip([finance_research, get_todays_date], results):
print(f"{fn.__name__}: tool_id={r.tool_id} v{r.version_number}")
finance_research: tool_id=c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f v1 committed=True
get_todays_date: tool_id=d0a800d5-a484-4d4f-a9e2-05a715775682 v1 committed=True
The docstring becomes the model-facing description, the signature becomes the JSON Schema, and executor is automatically set to "client".
Use hub.tools.create() and hub.tools.commitVersion() to register tools. Node's SDK doesn't derive schemas from TypeScript signatures, so you pass the schema explicitly:
import { acruxcore } from '@acruxcoreai/sdk';
const hub = new acruxcore();
// finance_research
const tool1 = await hub.tools.create({
name: "finance_research",
description: "Search Yahoo Finance news for a ticker symbol.",
});
await hub.tools.commitVersion(tool1.id, {
description: "Search Yahoo Finance news for a ticker symbol.",
parametersSchema: {
type: "object",
properties: {
ticker_symbol: { type: "string", description: 'Stock ticker symbol, e.g. "AAPL".' },
},
required: ["ticker_symbol"],
},
executor: { type: "client" },
});
// get_todays_date
const tool2 = await hub.tools.create({
name: "get_todays_date",
description: "Get today's date.",
});
await hub.tools.commitVersion(tool2.id, {
description: "Get today's date.",
parametersSchema: { type: "object", properties: {} },
executor: { type: "client" },
});
{"id":"ca59ea4a-d86b-45be-b424-263f97651f12","toolId":"d0a800d5-a484-4d4f-a9e2-05a715775682","versionNumber":1,"description":"Get today's date.","parametersSchema":{"type":"object","properties":{}},"executor":{"type":"client"},"aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
2. Create the prompt over the API
The prompt's system message is what turns two tools into a ReAct-style agent: it tells the model to reason before acting, names both tools, and says when to reach for each. Binding both tools to the prompt means a single render call returns everything the loop needs.
You can also author prompts in the dashboard's visual editor, or declare tools in code with acrux.tool and have the first run auto-create the prompt.
- curl
- Python (SDK)
- Node (SDK)
curl -X POST "$ACRUXCORE_BASE_URL/prompts" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"react-agent-finance","description":"ReAct-style finance research agent, called directly against OpenAI (no gateway)."}'
curl -X POST "$ACRUXCORE_BASE_URL/prompts/6611c5f6-7f34-4e5e-b3b1-cb325c8070bd/versions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"messages": [
{"role":"system","content":"You are a financial research assistant. Reason step by step about what the question actually needs before answering. Use the finance_research tool to look up recent Yahoo Finance news for a stock ticker, and the get_todays_date tool whenever the question depends on today'"'"'s date (relative dates, whether markets are open, and similar). Only call a tool when its result is genuinely needed, then give a clear final answer grounded in what the tools returned."},
{"role":"user","content":"{{ question }}"}
]
}'
{"id":"53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4","promptId":"6611c5f6-7f34-4e5e-b3b1-cb325c8070bd","versionNumber":1,"variables":["question"],"model":null,"aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
Now connect the two tools. A binding is a live setting on the prompt rather than
part of a version, so this is one PUT per tool and there is nothing to commit
afterwards:
for TOOL in c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f d0a800d5-a484-4d4f-a9e2-05a715775682; do
curl -X PUT "$ACRUXCORE_BASE_URL/prompts/6611c5f6-7f34-4e5e-b3b1-cb325c8070bd/tools/$TOOL" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"tool_alias":"production"}'
done
Each binding lands as the prompt's default, so every alias of the prompt — including the two the first commit just created — calls both tools. See Connect a tool to a prompt for giving one alias a different build.
No model is bound on the version — that field points at the gateway's model registry, and this page never touches the gateway. The model id comes from PROVIDER_MODEL in the scripts below instead, and it belongs to whichever provider your base URL points at — the same arrangement as the OpenRouter model id in Build a RAG agent without the gateway.
The first version auto-creates production/staging aliases, so it's immediately renderable:
curl -X POST "$ACRUXCORE_BASE_URL/prompts/react-agent-finance/production/render" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"variables":{"question":"Is there any recent news on AAPL, and is today a weekday?"}}'
{
"messages": [
{"role":"system","content":"You are a financial research assistant. Reason step by step about what the question actually needs before answering. Use the finance_research tool to look up recent Yahoo Finance news for a stock ticker, and the get_todays_date tool whenever the question depends on today's date (relative dates, whether markets are open, and similar). Only call a tool when its result is genuinely needed, then give a clear final answer grounded in what the tools returned."},
{"role":"user","content":"Is there any recent news on AAPL, and is today a weekday?"}
],
"tools": [
{"type":"function","function":{"name":"finance_research","description":"Search Yahoo Finance news for a ticker symbol.","parameters":{"type":"object","required":["ticker_symbol"],"properties":{"ticker_symbol":{"type":"string","description":"Stock ticker symbol, e.g. \"AAPL\"."}}}}},
{"type":"function","function":{"name":"get_todays_date","description":"Get today's date.","parameters":{"type":"object","properties":{}}}}
],
"model": null,
"versionId": "53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4",
"versionNumber": 1
}
That tools array is already OpenAI's exact tools[].function shape — nothing to reformat before sending it to OpenAI.
hub.prompts.create() creates the shell, hub.prompts.commit_version() stores the messages, and hub.prompts.set_tool_binding() connects each tool. Use the tool ids from Step 3:
from acruxcore import AcruxCore
async with AcruxCore() as hub:
# Create the prompt shell
prompt = await hub.prompts.create(
"react-agent-finance",
description="ReAct-style finance research agent, called directly against OpenAI (no gateway).",
)
# Commit a version with the messages — the template only
version = await hub.prompts.commit_version(
prompt.id,
messages=[
{"role": "system", "content": "You are a financial research assistant. Reason step by step about what the question actually needs before answering. Use the finance_research tool to look up recent Yahoo Finance news for a stock ticker, and the get_todays_date tool whenever the question depends on today's date (relative dates, whether markets are open, and similar). Only call a tool when its result is genuinely needed, then give a clear final answer grounded in what the tools returned."},
{"role": "user", "content": "{{ question }}"},
],
)
# Connect both tools — the default binding, inherited by every alias
for tool_id in ("c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f", "d0a800d5-a484-4d4f-a9e2-05a715775682"):
await hub.prompts.set_tool_binding(prompt.id, tool_id, tool_alias="production")
print(f"prompt_id={prompt.id} v{version.version_number}")
Full script: setup_prompt.py.
{"id":"53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4","promptId":"6611c5f6-7f34-4e5e-b3b1-cb325c8070bd","versionNumber":1,"variables":["question"],"model":null,"aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
No model is bound — that field points at the gateway's model registry, and this page never touches the gateway. The first version auto-creates production/staging aliases, so it's immediately renderable:
rendered = await hub.prompts.render("react-agent-finance", "production",
{"question": "Is there any recent news on AAPL, and is today a weekday?"})
{
"messages": [
{"role":"system","content":"You are a financial research assistant. Reason step by step..."},
{"role":"user","content":"Is there any recent news on AAPL, and is today a weekday?"}
],
"tools": [
{"type":"function","function":{"name":"finance_research","description":"Search Yahoo Finance news for a ticker symbol.","parameters":{"type":"object","required":["ticker_symbol"],"properties":{"ticker_symbol":{"type":"string","description":"Stock ticker symbol, e.g. \"AAPL\"."}}}}},
{"type":"function","function":{"name":"get_todays_date","description":"Get today's date.","parameters":{"type":"object","properties":{}}}}
],
"model": null,
"versionId": "53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4",
"versionNumber": 1
}
That tools array is already OpenAI's exact tools[].function shape — nothing to reformat before sending it to OpenAI.
Same shape. hub.prompts.create() creates the shell, hub.prompts.commitVersion() stores the messages, and hub.prompts.setToolBinding() connects each tool:
import { acruxcore } from '@acruxcoreai/sdk';
const hub = new acruxcore();
// Create the prompt shell
const prompt = await hub.prompts.create({
name: "react-agent-finance",
description: "ReAct-style finance research agent, called directly against OpenAI (no gateway).",
});
// Commit a version with the messages — the template only
const version = await hub.prompts.commitVersion(prompt.id, {
messages: [
{ role: "system", content: "You are a financial research assistant. Reason step by step about what the question actually needs before answering. Use the finance_research tool to look up recent Yahoo Finance news for a stock ticker, and the get_todays_date tool whenever the question depends on today's date (relative dates, whether markets are open, and similar). Only call a tool when its result is genuinely needed, then give a clear final answer grounded in what the tools returned." },
{ role: "user", content: "{{ question }}" },
],
});
// Connect both tools — the default binding, inherited by every alias
for (const toolId of ["c6ffbbff-7e5f-4e6e-acbe-1fd1a027632f", "d0a800d5-a484-4d4f-a9e2-05a715775682"]) {
await hub.prompts.setToolBinding(prompt.id, toolId, { toolAlias: "production" });
}
console.log(`prompt_id=${prompt.id} v${version.versionNumber}`);
{"id":"53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4","promptId":"6611c5f6-7f34-4e5e-b3b1-cb325c8070bd","versionNumber":1,"variables":["question"],"model":null,"aliases":[{"alias":"production","versionNumber":1},{"alias":"staging","versionNumber":1}]}
The first version auto-creates production/staging aliases, so it's immediately renderable:
const rendered = await hub.prompts.render("react-agent-finance", "production",
{ question: "Is there any recent news on AAPL, and is today a weekday?" });
{
"messages": [
{"role":"system","content":"You are a financial research assistant. Reason step by step..."},
{"role":"user","content":"Is there any recent news on AAPL, and is today a weekday?"}
],
"tools": [
{"type":"function","function":{"name":"finance_research","description":"Search Yahoo Finance news for a ticker symbol.","parameters":{"type":"object","required":["ticker_symbol"],"properties":{"ticker_symbol":{"type":"string","description":"Stock ticker symbol, e.g. \"AAPL\"."}}}}},
{"type":"function","function":{"name":"get_todays_date","description":"Get today's date.","parameters":{"type":"object","properties":{}}}}
],
"model": null,
"versionId": "53df8bc8-0d4a-4c00-91c5-bfa7e9c8bbf4",
"versionNumber": 1
}
That tools array is already OpenAI's exact tools[].function shape — nothing to reformat before sending it to OpenAI.
3. Run the agent
There's no SDK call that does this loop for you here — runToolLoop() only exists for the gateway and BYO-via-SDK paths. On this page every tab drives the loop by hand:
- Render the prompt (AcruxCore) — one call, gets you messages + tool schemas + the version id.
- Complete —
POST $PROVIDER_BASE_URL/chat/completions, straight to your provider, with your own key. Never through AcruxCore. - Report the
llmspan yourself — because no gateway saw that call, nothing records it unless you do.POST /traceswithkind: "llm", the model,providerset to the host from your base URL (derive it rather than typing it, or the span will name a provider you stopped using), and the token usage the provider returned. - If the model asked for a tool, run it locally, report a
kind: "tool"span onto the same trace, feed the result back as arole: "tool"message, and go back to step 2. - Once the model stops asking for tools, print its answer.
This is exactly what the SDK's chat() does automatically on the BYO path — pass a
provider: { apiKey, baseUrl } option (per-call, or once on the client) instead of routing
through AcruxCore's gateway, and the SDK calls that provider directly, mints a fresh trace
id locally, and reports one llm span per round with costUsd always empty since the
gateway never saw the call. See Build a RAG agent without the
gateway for a full worked example of that option.
Here you do it explicitly, in two languages, with no SDK at all.
finance_research hits the real Yahoo Finance news endpoint in both languages, and in both it is the same two calls written out by hand: a plain GET to fc.yahoo.com for a session cookie, then a POST to finance.yahoo.com's internal news-stream API, both with a browser-shaped User-Agent. This particular endpoint needs no crumb token, just the cookie.
Those two calls are also the whole of what a library wrapper would do here, which is why there is no library. It is worth knowing they have to happen in that order and share a session: that is exactly why this tool is a client executor and not an HTTP one — the gateway can describe one request, not a request that carries a cookie picked up by a previous one.
- curl
- Python
Run standalone first, to see what the real tool returns before wiring it into a loop — a GET to pick up a session cookie, then a POST to Yahoo's own news-stream API:
jar=$(mktemp)
curl -s -c "$jar" -A "Mozilla/5.0" "https://fc.yahoo.com" -o /dev/null
curl -s -b "$jar" -A "Mozilla/5.0" -X POST \
"https://finance.yahoo.com/xhr/ncp?queryRef=latestNews&serviceKey=ncp_fin" \
-H "Content-Type: application/json" \
-d '{"serviceConfig":{"snippetCount":3,"s":["AAPL"]}}' \
| jq -r '.data.tickerStream.stream[] | "\(.content.title)\n\(.content.summary)"'
rm -f "$jar"
Apple stock slides nearly 10% as Cook warns of memory shortage impact
Apple reported its Q3 earnings on Thursday.
Apple's iPhone leasing program: How it works, what to consider
Apple is partnering with Klarna to offer device leases with low monthly payments. Here's what experts say consumers should consider before signing up.
Apple CEO Tim Cook says this '100-year flood' won't be receding anytime soon
Memory chip supply constraints remain a problem for Apple.
date -u +%F
2026-08-01
The full loop renders the prompt, sends each completion straight to your provider, reports the
llm span itself since no gateway saw the call, runs whichever tool the model asked for
locally, reports a tool span onto the same trace, and repeats until the model stops
asking for tools — see react_agent.sh on
GitHub
for the complete source.
Run it:
./react_agent.sh "Is there any recent news on AAPL, and is today a weekday?"
Question: Is there any recent news on AAPL, and is today a weekday?
Fetched 2 message(s) + 2 tool(s) [finance_research, get_todays_date]
-> finance_research({"ticker_symbol":"AAPL"})
Apple stock slides nearly 10% as Cook warns of memory shortage impact
-> get_todays_date({})
2026-08-01
Assistant: Recent news on Apple Inc. (AAPL):
1. **Stock Performance**: Apple stock has slid nearly 10% following a warning from CEO Tim Cook about the impact of memory shortages.
2. **Earnings Report**: Apple reported its Q3 earnings on a recent Thursday.
3. **iPhone Leasing Program**: Apple has launched an iPhone leasing program in partnership with Klarna, aimed at providing low monthly payment options for consumers.
4. **Supply Chain Issues**: Tim Cook has highlighted that ongoing memory chip supply constraints are significant and might be a long-term issue, referring to it as a '100-year flood.'
As for today's date, it is **August 1, 2026**, which is a weekday (Monday).
Final Answer: There is significant recent news about Apple (AAPL), especially regarding stock performance and supply issues, and today is a weekday.
(2 model turn(s), trace dbb3f6af-5d43-4242-937b-74560bceecef)
(The model's arithmetic on the day of the week is its own — August 1, 2026 actually falls on a Saturday. Tracing tells you what the model said and which tools it called; it doesn't grade the answer.)
Run standalone first, to see what the real tool returns before wiring it into a loop. The same two calls as the curl tab, in the same order:
import requests
from datetime import datetime
YAHOO_NEWS_URL = "https://finance.yahoo.com/xhr/ncp?queryRef=latestNews&serviceKey=ncp_fin"
async def finance_research(ticker_symbol: str, limit: int = 3) -> str:
"""Recent Yahoo Finance headlines for one ticker, as plain text for the model."""
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0"})
session.get("https://fc.yahoo.com", timeout=15) # 1. pick up the session cookie
res = session.post( # 2. the real news call
YAHOO_NEWS_URL,
json={"serviceConfig": {"snippetCount": limit, "s": [ticker_symbol]}},
timeout=20,
)
res.raise_for_status()
stream = res.json()["data"]["tickerStream"]["stream"]
return "\n\n".join(
f"{i['content']['title']}\n{i['content'].get('summary') or ''}".strip()
for i in stream[:limit]
)
async def get_todays_date() -> str:
"""Get today's date."""
return datetime.now().strftime("%Y-%m-%d")
=== finance_research(AAPL) ===
AAPL Q2 Deep Dive: Supply Chain Strains and Product Demand Set Stage for Deceleration
iPhone and iPad maker Apple (NASDAQ:AAPL) reported revenue ahead of Wall Street's expectations in Q2 CY2026, with sales up 16.4% year on year to $109.4 billion. Its non-GAAP profit of $2.02 per share was 7% above analysts' consensus estimates.
Jim Cramer Can't Help But Gloat At Apple Inc (NASDAQ:AAPL)'s AI Developments
Ever the Apple Inc. (NASDAQ:AAPL) bull, Jim Cramer is nothing but ecstatic about the firm's recent fortunes. With a market capitalization of $4.90 trillion when Cramer made his remarks, the firm was the most valuable company in the world once again after losing the place to NVIDIA. Most of the debate surrounding Apple Inc (NASDAQ:AAPL) […]
=== get_todays_date() ===
2026-08-01
The full loop does the same thing in Python: render the prompt, send each completion
straight to your provider, report the llm span itself, run whichever tool the model asked for
locally (finance_research calling Yahoo's own news endpoint with requests),
report a tool span onto the same trace, and repeat until the model stops asking for
tools — see react_agent.py on
GitHub
for the complete source.
Run it:
python react_agent.py "Is there any recent news on AAPL, and is today a weekday?"
Question: Is there any recent news on AAPL, and is today a weekday?
Fetched 2 message(s) + 2 tool(s) [finance_research, get_todays_date]
-> finance_research({'ticker_symbol': 'AAPL'})
AAPL Q2 Deep Dive: Supply Chain Strains and Product Demand Set Stage for Deceleration
iPhone and iPad maker Apple (NASDAQ:AAPL) reported revenue ahead of Wall Street's expectations in Q2 CY2026, with
-> get_todays_date({})
2026-08-01
Assistant: Recent news on Apple Inc. (AAPL) includes their Q2 earnings report for the calendar year 2026, which exceeded Wall Street's expectations. They reported revenues of $109.4 billion, which is a 16.4% increase year on year, along with a non-GAAP profit of $2.02 per share, 7% above analysts' consensus estimates. Additionally, Jim Cramer expressed excitement about Apple's developments in AI, noting its significant market capitalization of $4.90 trillion, making it the most valuable company in the world once more, surpassing NVIDIA.
As for today's date, it is August 1, 2026, which falls on a Saturday. Therefore, today is not a weekday.
(2 model turn(s), trace 845d27bc-e919-4f73-9bb3-5b2ee9d6b43c)
4. Inspect the trace
Open Traces and every run from this page shows up as react-agent-finance — one row per script run above, four spans each, and no cost column, because BYO calls never touch the gateway's rate card:

Open one and the model → tool → tool → model rhythm is exactly what the loop above did: the first llm span asked for both tools in the same turn, finance_research and get_todays_date ran and reported back, then a second llm span produced the final answer. This particular trace is from the curl run above, so finance_research's output below is the real Yahoo Finance news text that run got back — expanding the span shows the real argument the model chose and the real result:

Every span here was reported by the script, not by a gateway — that's the whole difference between this page and the rest of the tool-calling tutorials on this site.
What's next
- The next tutorial in this series, Build a configurable ReAct agent, takes the same two tools through the dashboard and the gateway instead — no BYO key, routed models, budgets, and dollar cost on every span.
- Build a tool-calling agent in Python (no SDK) — the same hand-rolled loop shape, but through the gateway, where the
llmspan is recorded for you. - Build a RAG agent without the gateway — another BYO page, this time with the SDK doing the tracing and the loop for you.
- API details: see Prompts, Tools, and Traces in the API Reference.