AI agents (prompts)¶
The system prompt is the only text the model reads on every turn of the
loop, and it is the one that shows up least in the code: a loose string in the
first example's Agent(...), and then a SERVICE_AGENT_PROMPT imported from a
prompts/ folder that the architecture page names
without showing.
This page shows the folder, what goes in each file, how to compose a prompt that depends on who is asking, and how to test that it reached the model.
Before this page
AI agents up to Pydantic-typed tools, and
AI agents (architecture) for the src/ai/ layout
the prompts/ folder lives in.
No example here loads a model: they all run on the ScriptedBackend from
AI agents (testing), which keeps what the agent sent on
each call.
What the model receives¶
Before writing a prompt, look at what actually reaches the backend. There are
three things, and only one of them is your system_prompt:
import asyncio
import json
from pydantic import Field
from tempest_fastapi_sdk.agents import Agent, AgentContext, Skill, tool
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from tempest_fastapi_sdk.schemas import BaseSchema
class SearchArgs(BaseSchema):
"""Arguments for the catalogue search."""
name: str = Field(description="Partial match on the service title.")
@tool("search_services", "Search the public service catalogue by name.")
async def search_services(args: SearchArgs, context: AgentContext) -> str:
"""Return a canned catalogue line."""
return f"1 service: {args.name} | Picos/PI"
invoicing = Skill(
name="invoicing",
description="Read and validate Brazilian invoices (NF-e).",
instructions="The full NF-e guide.",
)
async def main() -> None:
"""Run once against a scripted backend and print what it received."""
backend = ScriptedBackend([replies("ok")])
agent = Agent(
backend,
tools=[search_services],
skills=[invoicing],
system_prompt="You are the service-catalogue assistant.",
)
await agent.run("Is there an electrician in Picos?")
print("--- system")
print(backend.system_prompts[0])
print("--- user")
print(backend.prompts[0])
print("--- tools")
print(backend.specs_seen[0])
print(json.dumps(search_services.to_spec(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
asyncio.run(main())
--- system
You are the service-catalogue assistant.
You have skills available. Each is a set of instructions and tools you can load with the 'load_skill' tool when the task needs it:
- invoicing: Read and validate Brazilian invoices (NF-e).
Load a skill before doing work that falls under it. Do not load skills you do not need.
--- user
Is there an electrician in Picos?
--- tools
['search_services', 'load_skill']
{
"type": "function",
"function": {
"name": "search_services",
"description": "Search the public service catalogue by name.",
"parameters": {
"description": "Arguments for the catalogue search.",
"properties": {
"name": {
"description": "Partial match on the service title.",
"title": "Name",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
}
}
Piece by piece:
systemis yoursystem_prompt, plus the block the SDK appends when the agent has skills. You do not write that block, but it is there — which is why it pays to look at the real output instead of assuming.useris the goal passed torun(). It changes on every run; the prompt does not.- The tools travel outside the prompt, as specs: the name, the
@tooldescriptionand the arguments' JSON-schema. Notice that even theSearchArgsdocstring reached the model, as the parameters'description.load_skillshows up because the agent has a skill.
The practical consequence of the third item: the prompt does not need to repeat the schema. The model already gets the name, type and description of every argument. What the prompt adds is what the schema does not say — when to use each tool.
The system message is the same on every turn
The system message is built once per run and resent unchanged on every
model call. Measured on a three-turn run: the three
backend.system_prompts were identical. That is why data that changes
mid-run does not belong in the prompt (more on that
below).
The prompts/ folder¶
One prompt per agent, and one file with the rules they all share:
src/ai/prompts/
├── __init__.py # re-exports the constants
├── base.py # rules common to every agent
├── service.py # SERVICE_AGENT_PROMPT
└── support.py # SUPPORT_AGENT_PROMPT
"""Text blocks every agent in this service shares."""
TOOL_FAILURE_RULE: str = (
"When a tool fails, read the error and change the call; "
"do not repeat the same call."
)
NO_GUESSING_RULE: str = (
"Answer only from what the tools returned. "
"If they did not bring the answer, say you could not find it."
)
ANSWER_FORMAT_RULE: str = "Answer in English, in at most three sentences, no markdown."
BASE_RULES: str = "\n".join(
[TOOL_FAILURE_RULE, NO_GUESSING_RULE, ANSWER_FORMAT_RULE],
)
"""System prompt of the service-catalogue agent."""
from src.ai.prompts.base import BASE_RULES
SERVICE_AGENT_PROMPT: str = f"""\
You are the platform's service-catalogue assistant.
Scope: the public service catalogue and the services the user published.
For anything else, say you cannot help with that.
Tools:
- search_services: questions about the public catalogue ("is there an electrician in Picos?").
- get_my_services: questions about what the user published.
{BASE_RULES}"""
"""System prompt of the support agent."""
from src.ai.prompts.base import BASE_RULES
SUPPORT_AGENT_PROMPT: str = f"""\
You are the platform's support desk.
Scope: account, payment and how-to questions about the site.
For questions about the service catalogue, say the catalogue assistant answers those.
Tools:
- search_help: search the FAQ before answering any how-to question.
- open_ticket: only when the FAQ did not solve it and the user asks for a human.
{BASE_RULES}"""
from src.ai.prompts.base import (
ANSWER_FORMAT_RULE,
BASE_RULES,
NO_GUESSING_RULE,
TOOL_FAILURE_RULE,
)
from src.ai.prompts.service import SERVICE_AGENT_PROMPT
from src.ai.prompts.support import SUPPORT_AGENT_PROMPT
__all__: list[str] = [
"ANSWER_FORMAT_RULE",
"BASE_RULES",
"NO_GUESSING_RULE",
"SERVICE_AGENT_PROMPT",
"SUPPORT_AGENT_PROMPT",
"TOOL_FAILURE_RULE",
]
What each choice buys:
- A module constant,
UPPER_SNAKE_CASE. The prompt is configuration, not logic: importingSERVICE_AGENT_PROMPTis whatsrc/ai/agents/service.pydoes, and a test imports the same constant to assert on it. - A separate
base.py. The tool-failure rule written once applies to both agents; fixed once, it is fixed for both. The test makes sure no prompt forgot to include it. __init__.pyre-exports. The rest of the service importsfrom src.ai.prompts import SERVICE_AGENT_PROMPT, never from the submodule — the same import rule as every package in the service.- Prompt text in the file, not in the database. Changing a prompt changes behaviour; in a file, the change goes through review and tests like any other.
To see the assembled result, run this next to src/:
import asyncio
from tempest_fastapi_sdk.agents import Agent
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from src.ai.prompts import SERVICE_AGENT_PROMPT
async def main() -> None:
"""Print the system message exactly as the backend receives it."""
backend = ScriptedBackend([replies("ok")])
await Agent(backend, system_prompt=SERVICE_AGENT_PROMPT).run("hi")
print(backend.system_prompts[0])
if __name__ == "__main__":
asyncio.run(main())
You are the platform's service-catalogue assistant.
Scope: the public service catalogue and the services the user published.
For anything else, say you cannot help with that.
Tools:
- search_services: questions about the public catalogue ("is there an electrician in Picos?").
- get_my_services: questions about what the user published.
When a tool fails, read the error and change the call; do not repeat the same call.
Answer only from what the tools returned. If they did not bring the answer, say you could not find it.
Answer in English, in at most three sentences, no markdown.
Anatomy of a prompt¶
The SERVICE_AGENT_PROMPT above has five parts. The order and the wording are
a writing judgement, not a measured result: this page did not compare one
prompt's hit rate against another's with a real model, and you should distrust
anyone who claims that without naming the model and the N.
| Part | Answers | In the example |
|---|---|---|
| Role | who the model is in this conversation | "service-catalogue assistant" |
| Scope | what is in, and what to do with the rest | "For anything else, say you cannot help" |
| When to use each tool | the decision the schema does not carry | search_services for the public catalogue, get_my_services for the user's own |
| Tool failure | what to do when the observation is an error | TOOL_FAILURE_RULE |
| Format | what the final answer should look like | ANSWER_FORMAT_RULE |
Three things here are not judgement, they are SDK mechanics:
- A tool name in the prompt must be the real name. The model calls by name;
a prompt that says
search_catalogwhen the tool issearch_servicesteaches the model to ask for a tool that does not exist, and the observation comes back asunknown tool 'search_catalog'; available: .... - The error the model reads is the one the tool wrote. The failure rule
only works if the
AgentToolErrormessage says what to change — AI agents (tools) shows messages written for that. - Tool descriptions live on the
@tool, not here. The prompt says when; thedescriptionand the schema say what. Writing the what twice is two descriptions that drift apart on the first edit.
What the SDK already puts in the prompt¶
DEFAULT_SYSTEM_PROMPT¶
Agent(...) without system_prompt= uses the DEFAULT_SYSTEM_PROMPT
constant:
You are a capable assistant working towards the user's goal. Use the available tools when they help and answer directly when they do not. When a tool fails, read the error and try a different approach rather than repeating the same call. When you have the answer, reply with it and stop calling tools.
Passing system_prompt= replaces that text — it does not append to it.
Measured: with system_prompt="X", the backend receives exactly "X". If you
want to keep the default's rules and add your own, compose:
from tempest_fastapi_sdk.agents import DEFAULT_SYSTEM_PROMPT, Agent
from tempest_fastapi_sdk.agents.testing import ScriptedBackend
SHOP_PROMPT: str = DEFAULT_SYSTEM_PROMPT + "\n\nAlways answer in English."
def build_agent() -> Agent:
"""Build an agent that keeps the default rules and adds one of its own.
Returns:
Agent: The agent, on a scripted backend for the example.
"""
return Agent(ScriptedBackend([]), system_prompt=SHOP_PROMPT)
The BASE_RULES in the prompts/ folder is the same idea with your service's
rules in place of the SDK's.
skills_prompt¶
With skills=, the agent appends the skills_prompt(skills) block to your
prompt — what you saw in this page's first output, starting at
You have skills available. The block comes after your text, and each
skill takes one line: - name: description. The skill's full instructions only
arrive when the model calls load_skill
(Skills).
Two effects on your prompt:
- Do not list the skills again. The block already lists them; repeating is the same information twice in the context, on every turn.
- A sentence of yours on when to load each skill still pays off, for the same reason as "when to use each tool" above.
DEFAULT_CRITIC_PROMPT¶
refine(worker, critic, goal) uses DEFAULT_CRITIC_PROMPT (or whatever you
pass as critic_prompt=). It does not go into the critic's system message:
it opens the user message, followed by GOAL: and WORK TO REVIEW:.
Measured with a critic built with system_prompt="C": the system received was
C, and the user message started with the DEFAULT_CRITIC_PROMPT text. A
critic prompt of your own must keep asking for the exact word APPROVED — that
is what refine decides on
(Loop: generate, critique, revise).
A prompt that depends on the request¶
facts_prompt and recall_prompt return text blocks to append to the
prompt — the user's facts, what earlier conversations might contribute. Both
depend on who is asking, and Agent takes the prompt in its constructor:
run() does not accept another one. So a per-request prompt is a
per-request agent, built by a function:
import asyncio
from tempest_fastapi_sdk.agents import (
Agent,
AgentBackend,
FactStore,
InMemoryFactStore,
facts_prompt,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from src.ai.prompts import SERVICE_AGENT_PROMPT
def build_agent(generator: AgentBackend, prompt: str) -> Agent:
"""Build the agent of one request around the process-wide generator.
Args:
generator (AgentBackend): The shared backend; it is reused, never copied.
prompt (str): The system prompt composed for this request.
Returns:
Agent: An agent whose prompt belongs to this request only.
"""
return Agent(generator, system_prompt=prompt, name="service-agent")
async def prompt_for(user_id: str, facts: FactStore) -> str:
"""Compose the fixed prompt with what is known about this user.
Args:
user_id (str): The authenticated caller.
facts (FactStore): Where durable facts live.
Returns:
str: The agent prompt plus the user's facts block.
"""
return SERVICE_AGENT_PROMPT + await facts_prompt(facts, subject=user_id)
async def main() -> None:
"""Serve two users with one generator and show each prompt's tail."""
facts = InMemoryFactStore()
await facts.put("city", "Picos/PI", subject="ana")
await facts.put("city", "Teresina/PI", subject="bruno")
generator = ScriptedBackend([replies("ok"), replies("ok")])
for user_id in ("ana", "bruno"):
agent = build_agent(generator, await prompt_for(user_id, facts))
await agent.run("Is there an electrician near me?")
for prompt in generator.system_prompts:
print(prompt.splitlines()[-2:])
if __name__ == "__main__":
asyncio.run(main())
One generator, two agents, two prompts. The expensive part — the model weights —
stays in runtime.py, one per process; what is built per request is just the
Agent object, which holds references to the generator and to the tools that
already existed.
What building the agent costs¶
Measured on the same i9-13900F under WSL2 as What each step costs, Python 3.11.12, median of 5 rounds:
8 tools + 3 skills: 3.28 µs (N=100000 x 5)
8 tools, no skills: 1.37 µs (N=100000 x 5)
facts_prompt + build: 12.18 µs (N=20000 x 5)
The cheapest model step measured on that page was 0.29 s (Ollama,
qwen2.5:0.5b, warm). Building the agent with eight typed tools and three
skills costs ~3.3 µs — five orders of magnitude below a single model turn. The
skills double the cost because the constructor builds the load_skill tool and
concatenates the skills_prompt block; the tools are not rebuilt, only the list
that references them.
The measurement script
import asyncio
import statistics
import timeit
from collections.abc import Callable
from pydantic import Field
from tempest_fastapi_sdk.agents import (
Agent,
AgentBudget,
AgentContext,
AgentTool,
InMemoryFactStore,
Skill,
facts_prompt,
tool,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend
from tempest_fastapi_sdk.schemas import BaseSchema
ROUNDS: int = 5
class SearchArgs(BaseSchema):
"""Arguments shared by the measured tools."""
query: str = Field(description="What to search for.", max_length=255)
city: str | None = Field(default=None, description="City name.")
page: int = Field(default=1, ge=1)
def make_tool(index: int) -> AgentTool:
"""Build one typed tool, the way a service builds them at import.
Args:
index (int): Suffix that keeps the tool names unique.
Returns:
AgentTool: The tool.
"""
@tool(f"tool_{index}", f"Tool {index}: search one domain.")
async def handler(args: SearchArgs, context: AgentContext) -> str:
"""Echo the query."""
return args.query
return handler
TOOLS: list[AgentTool] = [make_tool(index) for index in range(8)]
SKILLS: list[Skill] = [
Skill(
name=f"skill_{index}",
description=f"Domain {index}.",
instructions="x" * 2000,
tools=[make_tool(100 + index)],
)
for index in range(3)
]
GENERATOR: ScriptedBackend = ScriptedBackend([])
PROMPT: str = "You are the service assistant. " * 20
def build_agent(prompt: str, *, skills: list[Skill]) -> Agent:
"""Build the per-request agent around the shared generator.
Args:
prompt (str): The request's system prompt.
skills (list[Skill]): The skills to attach.
Returns:
Agent: A fresh agent; the generator is shared, not copied.
"""
return Agent(
GENERATOR,
tools=TOOLS,
skills=skills,
system_prompt=prompt,
budget=AgentBudget(max_steps=8, max_seconds=30.0),
)
def median_us(call: Callable[[], object], number: int) -> float:
"""Return the median cost of ``call`` in microseconds over ROUNDS rounds.
Args:
call (Callable[[], object]): What to time.
number (int): Calls per round.
Returns:
float: Median microseconds per call.
"""
rounds = [timeit.timeit(call, number=number) / number * 1e6 for _ in range(ROUNDS)]
return statistics.median(rounds)
def main() -> None:
"""Time the construction with and without skills, and with facts."""
loop = asyncio.new_event_loop()
facts = InMemoryFactStore()
loop.run_until_complete(facts.put("city", "Picos/PI", subject="ana"))
async def with_facts() -> Agent:
"""Compose the prompt from the store, then build."""
block = await facts_prompt(facts, subject="ana")
return build_agent(PROMPT + block, skills=SKILLS)
with_skills = median_us(lambda: build_agent(PROMPT, skills=SKILLS), 100_000)
without = median_us(lambda: build_agent(PROMPT, skills=[]), 100_000)
composed = median_us(lambda: loop.run_until_complete(with_facts()), 20_000)
print(f"8 tools + 3 skills: {with_skills:.2f} µs (N=100000 x {ROUNDS})")
print(f"8 tools, no skills: {without:.2f} µs (N=100000 x {ROUNDS})")
print(f"facts_prompt + build: {composed:.2f} µs (N=20000 x {ROUNDS})")
loop.close()
if __name__ == "__main__":
main()
Two earlier runs of the same script (Portuguese labels) gave 3.30 / 1.39 / 12.11 µs and 3.37 / 1.40 / 12.17 µs.
In the service¶
In the architecture layout the factory lives in
src/ai/agents/ and the controller calls it on every request. The run_sink
and the FactStore come in as parameters, as they do there:
from tempest_fastapi_sdk.agents import Agent, AgentRunSink, FactStore, facts_prompt
from src.ai.prompts import SERVICE_AGENT_PROMPT
from src.ai.runtime import agent_budget, generator
from src.ai.tools import get_my_services, search_services
async def build_service_agent(
user_id: str,
*,
facts: FactStore,
run_sink: AgentRunSink | None = None,
) -> Agent:
"""Build the service agent for one caller.
Args:
user_id (str): The authenticated caller; their facts join the prompt.
facts (FactStore): Where durable facts live.
run_sink (AgentRunSink | None): Where finished runs are recorded.
Returns:
Agent: A fresh agent around the process-wide generator and tools.
"""
prompt = SERVICE_AGENT_PROMPT + await facts_prompt(facts, subject=user_id)
return Agent(
generator,
tools=[search_services, get_my_services],
system_prompt=prompt,
budget=agent_budget(),
run_sink=run_sink,
name="service-agent",
)
from tempest_fastapi_sdk.agents import AgentRunSink, FactStore
from src.ai import build_service_agent, context_for
from src.db.models import UserModel
from src.schemas import AgentAnswerResponseSchema, AgentAskRequestSchema
class AIController:
"""Controller for the agent-backed endpoints."""
def __init__(self, facts: FactStore, run_sink: AgentRunSink | None) -> None:
"""Store the process-wide collaborators.
Args:
facts (FactStore): Where durable facts live.
run_sink (AgentRunSink | None): Where finished runs are recorded.
"""
self.facts: FactStore = facts
self.run_sink: AgentRunSink | None = run_sink
async def ask(
self,
user: UserModel,
data: AgentAskRequestSchema,
) -> AgentAnswerResponseSchema:
"""Answer a question with a prompt built for this caller.
Args:
user (UserModel): The authenticated caller.
data (AgentAskRequestSchema): The question.
Returns:
AgentAnswerResponseSchema: The answer plus what the run did.
"""
agent = await build_service_agent(
str(user.id),
facts=self.facts,
run_sink=self.run_sink,
)
run = await agent.run(data.question, context=context_for(user.id))
return AgentAnswerResponseSchema(
output=run.output,
succeeded=run.succeeded,
stop_reason=str(run.stop_reason),
tool_calls=run.tool_calls,
seconds=run.seconds,
)
With the ready-made router¶
The controller above is an endpoint of your own. When the only thing that
changes per request is the prompt, the SDK's router covers it:
make_agent_router accepts, in place of the Agent, a FastAPI dependency
that returns the request's Agent.
import asyncio
from fastapi import Depends, FastAPI, Header
from fastapi.testclient import TestClient
from tempest_fastapi_sdk.agents import (
Agent,
InMemoryAgentRunSink,
InMemoryFactStore,
make_agent_router,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from prompt_per_request import prompt_for
facts: InMemoryFactStore = InMemoryFactStore()
generator: ScriptedBackend = ScriptedBackend([replies("ok"), replies("ok")])
store: InMemoryAgentRunSink = InMemoryAgentRunSink(max_runs=50)
def current_user(x_user_id: str = Header()) -> str:
"""Return who is calling.
A stand-in for your real authentication dependency: trusting a header
is only acceptable behind a gateway that sets it.
"""
return x_user_id
async def agent_for(user_id: str = Depends(current_user)) -> Agent:
"""Build the agent of one request, with the caller's facts in the prompt."""
prompt = await prompt_for(user_id, facts)
return Agent(generator, system_prompt=prompt, run_sink=store, name="service-agent")
app: FastAPI = FastAPI()
app.include_router(
make_agent_router(agent_for, run_store=store, owner=current_user),
)
def main() -> None:
"""Call the router as two users and show what each run received."""
asyncio.run(facts.put("city", "Picos/PI", subject="ana"))
client = TestClient(app)
for user_id in ("ana", "bruno"):
client.post(
"/api/agent/run",
json={"goal": "Is there an electrician near me?"},
headers={"X-User-Id": user_id},
)
for prompt in generator.system_prompts:
print(prompt.splitlines()[-2:])
print([run.owner for run in store.recent()])
if __name__ == "__main__":
main()
['What you already know:', '- city: Picos/PI']
['Answer only from what the tools returned. If they did not bring the answer, say you could not find it.', 'Answer in English, in at most three sentences, no markdown.']
['bruno', 'ana']
Ana got her fact; Bruno, who has none, got the bare SERVICE_AGENT_PROMPT,
ending on the last two rules of BASE_RULES. Both runs landed in the store
with the right owner (most recent first).
What to know about the factory:
- It is a dependency like any other. It can take the
Request, declare its ownDepends(...)(the user, a session), bedeforasync def. Its type isAgentDependency. - The
owner=dependency is resolved once. The factory above declaresDepends(current_user), the same function passed asowner=; FastAPI caches the value for the request, so the factory and the run record see the same user. - Only
/runand/run/streamcall the factory.GET /runsand the artifact download read therun_storewithout building an agent — which is why every agent the factory builds must write to the same sink (run_sink=store). make_agent_router(agent)is unchanged. A ready-madeAgentis served as always, the same instance on every request.
When the endpoint is still yours
The router seeds only context.owner. A tool that reads the caller from
state (the context_for + require_user_id of the
architecture)
still needs the controller above — a per-request prompt is no longer a
reason for it.
What not to put in the prompt¶
Identity and permission. "The user is 42, only show their services" is a
sentence, and the model can ignore a sentence. Who is asking travels on the
AgentContext and the tool reads it with require_user_id
(policy.py).
A fact in the prompt (city: Picos/PI) helps the model answer; it does not
restrict what the tool returns.
Data that changes during the run. The system message is the same on every turn of a run (measured above). Stock, balance, an order's status: whatever can change while the agent works is a tool result, read at the moment the model needs it.
The tools' schema. It already goes in the specs, as this page's first output showed.
Secrets. The prompt is text the model reads, and nothing in the SDK stops the model from repeating it in the answer. API keys and credentials stay in the tool's code.
Testing the prompt¶
The prompt is a constant, so its test is fast — and the ScriptedBackend lets
you assert on what reached the model:
import pytest
from tempest_fastapi_sdk.agents import InMemoryFactStore
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from prompt_per_request import build_agent, prompt_for
from src.ai.prompts import BASE_RULES, SERVICE_AGENT_PROMPT, SUPPORT_AGENT_PROMPT
def test_every_agent_prompt_carries_the_base_rules() -> None:
"""A prompt that forgot the shared rules is a drift, caught here."""
assert BASE_RULES in SERVICE_AGENT_PROMPT
assert BASE_RULES in SUPPORT_AGENT_PROMPT
@pytest.mark.asyncio
async def test_each_request_sees_only_its_own_facts() -> None:
"""Ana's facts must not reach the run started for Bruno."""
facts = InMemoryFactStore()
await facts.put("city", "Picos/PI", subject="ana")
generator = ScriptedBackend([replies("ok"), replies("ok")])
await build_agent(generator, await prompt_for("ana", facts)).run("hi")
await build_agent(generator, await prompt_for("bruno", facts)).run("hi")
assert "Picos/PI" in generator.system_prompts[0]
assert generator.system_prompts[1] == SERVICE_AGENT_PROMPT
The second test asserts something only the real output shows: with no fact at
all, facts_prompt returns an empty string and Bruno's prompt is exactly
SERVICE_AGENT_PROMPT, with no orphan heading.
What this test does not answer
Whether the model follows the prompt — picks the tool the prompt says, respects the format — is a question for the real model, in a separate, marked suite: What about a real model?
Recap¶
- The model receives three things: the system message (your prompt plus
what the SDK appends), the goal and the tool specs.
ScriptedBackendshows all three without a model. src/ai/prompts/has one file per agent and abase.pywith the shared rules, asUPPER_SNAKE_CASEconstants re-exported from__init__.py.- The prompt says when; the
@toolsays what. A tool name quoted in the prompt must be the real name. system_prompt=replacesDEFAULT_SYSTEM_PROMPT; compose with it if you want to keep the SDK's rules. Skills append their own block after your text.- A per-request prompt is a per-request agent: a function builds the
Agentaround the process's generator. Measured, it costs ~3.3 µs with eight tools and three skills.make_agent_routertakes that function as a dependency, so the ready-made endpoints stay. - Identity, permission, volatile data and secrets stay out of the prompt.
- Test the constant and what reached the model; whether the model obeys is another suite.
See also: AI agents (tools) for writing the error message
the failure rule tells the model to read, and
AI agents (advanced)
for the facts_prompt and recall_prompt blocks.