On this page
A Cradle agent is a FastAPI backend that builds a BaseAgent itself, keeps its own users and chat history, and talks to HippoDb through the manager with an API key. This page covers the SDK features such an agent needs, in the order you meet them. It leaves out what the Cradle starter does not use: the Agent Creator, the agent network (the starter is networked: false), and the terminal runner (AgentRunner).
How to read it: section 1 is a map. Sections 2 to 7 are the wiring you do once. Sections 8 to 12 are what happens on every chat request. Sections 13 to 16 are configuration, diagnosis and upgrade notes.
1. At a glance#
| Feature | What it is for | Where it lives in a Cradle project |
|---|---|---|
HippoClient |
The connection to HippoDb: read, write, create and delete fabrics | built wherever the project talks to HippoDb: the chat handler, account deletion, anchoring, the seed script |
BaseAgent |
The tool-use loop: one chat() call per message |
built in backend/agent/chat.py (_make_sdk_handler), driven by the chat handler |
ToolRegistry, BaseTool |
The tools the model can call: the SDK's memory tools and your business tools | your tool modules, for example backend/agent/tools.py |
ToolDiscovery |
Stores the tools in memory and offers the model only the relevant ones each turn | built next to the registry |
RoleConfig |
The agent's identity, system prompt and tool scopes | backend/agent/role.py |
| Memory tools | memory_remember, memory_think and the rule and conflict tools |
registered with get_builtin_tools() |
| Seeding | The first knowledge in a fabric: domain concepts, intents, bridges, workflows, rules | backend/agent/seed/, loaded by a seed script |
| Events and streaming | Live text and progress for the chat UI, over SSE | agent.event_queue drained by the handler |
| Stop and cancel | A stop button, and a clean end when the client disconnects | agent.stop() and task cancellation |
| Reasoning effort | A per-message "think harder" choice in the UI | chat(..., reasoning_effort=...) |
| Typed errors | Handling a busy model, a bad key or a lost fabric without matching text | LlmError family, FabricNotFoundError |
| Tool limits and history window | Keeping every LLM call small | BaseAgent(max_total_tools=..., history_window=...) |
2. Install and versions#
- Pin the SDK exactly in
backend/pyproject.toml, as the Cradle rules require (make installormake install-sdkinstalls it, whichever your project uses), and letuvupdate the lock:
cd backend && uv add hippodb-agent-sdk==2.0.0
- Python 3.12 or newer. LiteLLM comes with the SDK (it needs
litellm>=1.104.2); do not pin it yourself. The newest Claude models fail with older LiteLLM versions. - Check what is installed:
backend/.venv/bin/python -c "import hippodb_agent; print(hippodb_agent.__version__)". - The documentation ships inside the package.
python -m hippodb_agent docsprints its folder, andpython -m hippodb_agent docs --openopens the release notes in a browser. - The SDK no longer ships a
defaults.env. Settings come from your ownSettingsclass:BaseAgent,ToolDiscoveryand the LLM client never read.env. If a comment in your.env.examplesays the SDK'sdefaults.envis the lowest precedence, that comment is out of date.
3. Connect to HippoDb#
HippoClient is the only thing that talks to HippoDb. Build every one through a single helper, so the API key cannot be forgotten at a new call site (for example a make_hippo_client(settings, fabric) function in agent/fabric.py, used by the chat handler, account deletion, anchoring and the seed script):
from hippodb_agent.memory.client import HippoClient
def make_hippo_client(settings, fabric: str) -> HippoClient:
return HippoClient(base_url=settings.hippodb_url, fabric=fabric,
api_key=settings.hippodb_api_key or None)
The API key#
- Point
HIPPODB_URLat the manager, not at an engine pod. Every request to it carriesAuthorization: Bearer hdb_live_.... - Pass the key in code. Pydantic reads
.envinto yourSettingsobject, not intoos.environ, so the SDK's fallback to theHIPPODB_API_KEYenvironment variable does not see a key that only lives in.env. - No key, or a bad, expired or revoked one, is a 401. The body's
errorsays which (missing_credential,invalid_credential). A key without the scope it needs is a 403. - The key needs the scopes
fabric:read,fabric:writeandfabric:delete, and a fabric cap (max_fabrics, 25 by default) that fits your users: a per-user agent creates one fabric per user. - Only
/admin/manager/healthis open./health,/fabricsand every other route answer 401 without a key, so a liveness or readiness probe needs the key or that path. - A fabric call that fails because of the key is an
httpx.HTTPStatusErrorwith status 401 or 403, and the SDK logs a WARNING with the manager's reason (memory.client).
Fabrics#
hippo = make_hippo_client(settings, fabric_name)
await hippo.create_fabric() # idempotent: 201 whether the fabric is new or not
await hippo.delete_fabric(name) # permanent, no undo (account deletion)
fabrics = await hippo.list_fabrics() # always a list of dicts: [{"name": ...}, ...]
create_fabric()cannot tell a new fabric from an existing one. Only a failure is worth reporting.delete_fabric()removes the database, the search index and the data files. There is no way to delete a single concept.- Fabric names come from your own rules (
APP_ENV,FABRIC_NAME, the user id). The SDK does not build them.
A lost fabric#
If a user's fabric is deleted while the process runs, the next call raises FabricNotFoundError. It comes out of agent.chat(), discovery.discover() and index_tools() and from the tools' own HippoDb calls. The SDK recognises it from the 404 body, and only when the 404 names your fabric; a missing concept is not mistaken for it.
from hippodb_agent import FabricNotFoundError
try:
reply = await agent.chat(text)
except FabricNotFoundError:
await seed_load.run(settings) # create the fabric again and reseed it
reply = await agent.chat(text) # retry the turn once
Data in a deleted fabric is gone. Re-seeding only makes the agent work again.
Timeouts and retries#
HippoClient defaults to a 60 second request timeout and a 10 second connect timeout. A request whose connection cannot be opened is retried twice with a short backoff; nothing else is retried, so a write is never sent twice. Both are arguments (timeout=, connect_retries=).
4. Build the agent#
Build the parts in this order. The full, tested example is in Building an Agent Without AgentRunner; this is the shape.
registry = ToolRegistry()
registry.register_many(get_builtin_tools(include_discover_more=True)) # memory tools + discover_more_tools
registry.register_many(get_manage_task_tools(settings, user_id)) # your business tools
registry.bind_all(hippo=hippo)
registry.bind_hippo(hippo)
await registry.warmup() # new in 2.0.0
discovery = ToolDiscovery(hippo, registry, extra_always_on=["manage_task"])
await discovery.index_tools()
role = RoleConfig(name=..., display_name=..., tool_scopes=["manage_"],
fabric=fabric_name, system_prompt=system_prompt())
agent = BaseAgent(role=role, registry=registry, discovery=discovery,
model=settings.model, api_key=llm_key,
manifest_fn=hippo.manifest, timeout=45,
enable_streaming=settings.enable_streaming)
Things that catch people out:
tool_scopesis a prefix list. It limits business tools to names starting with those prefixes. The SDK's ownmemory_,os_,calc_andnetwork_tools anddiscover_more_toolsare always in scope. An empty list hides every business tool; it does not mean "no restriction".- A tool the agent must always have goes in
extra_always_on(orRoleConfig.always_on_tools). Always-on tools are sent every turn and are never dropped by the tool limits. Anything else is chosen by discovery for each message. await discovery.index_tools()stores your tools in the fabric asTOOLconcepts. In 2.0.0discover()calls it for you, so a per-turn call in the chat handler is no longer needed. It stays harmless: after the first call per fabric it costs nothing.{manifest}is empty unless you fill it. If the system prompt has a{manifest}placeholder, passmanifest_fn=hippo.manifestandawait agent.refresh_manifest()after building the agent. In 2.0.0 the manifest is one line of fabric statistics (Fabric: x | Concepts: n | Edges: m), so many projects simply leave the placeholder out.- One turn at a time per agent. An agent keeps state for the turn in progress (which tools it offered and used,
last_turn, the event queue). Two turns on the sameBaseAgentat the same moment overwrite each other's state. Keep a lock aroundchat()(anasyncio.Lock), or build one agent per user. A per-user fabric layout suits one agent per user. - Model routing needs the router object.
BaseAgent(model=...)on its own ignoresLLM_ROUTING_MODELS. When routing is configured, passllm=ModelRouter(routing_models=..., router_model=...)instead ofmodel=, in an if/else that covers both cases.
5. Tools#
Your business tools#
A tool is a class with a name, a description for the model, a JSON schema for its input, and an async execute:
from hippodb_agent import BaseTool
class ManageTask(BaseTool):
name = "manage_task"
description = "Create, update or close a task for the user"
input_schema = {"type": "object", "properties": {"action": {"type": "string"}}, "required": ["action"]}
atomic = True # finish even if the turn is cancelled
async def execute(self, inp: dict) -> dict:
...
return {"task_id": task.id}
- Return a dict. An exception becomes
{"error": "..."}for the model, with the exception type when it has no message, so the model can recover. AFabricNotFoundErroris the exception to that rule: it propagates to your handler. - Set
atomic = Trueon a tool that writes to your database: if the client disconnects, the tool still finishes and its result is recorded. - Optional hints for discovery (never sent to the model):
domain(tools of one domain are linked together),related(tools often used together),keywords(synonyms the name and description lack) andexamples(phrases a user might say). Changing them reaches fabrics that were indexed earlier. - A tool added in a later release is found in existing fabrics. A tool whose name sits inside another concept's label (
order_listandrecurring_order_list) is reported in the log and indiscovery.last_index_reportinstead of being merged.
The SDK's memory tools#
get_builtin_tools(include_discover_more=True) registers these for the model:
| Tool | What the model uses it for |
|---|---|
memory_remember |
Store what the user said as a concept with facts and timeline events, in the right cluster |
memory_think |
Recall: by concept, keywords or cluster; as_of for "what was true at that time"; temporal_direction for before and after |
memory_list_concepts |
Browse memory by type, tag, property or label pattern |
memory_learn_rule, memory_forget_rule |
Save or retire a behavioural rule ("always ask before deleting") |
memory_list_conflicts, memory_resolve_conflict |
Show duplicates the engine could not merge, and apply the user's choice |
memory_promote_rule, memory_approve_rule |
Turn a soft rule into an enforced one, and approve it (found by discovery, not always sent) |
discover_more_tools |
Ask for more tools when the offered ones are not enough |
In a Cradle project the always-on set is these memory tools (except promote and approve) plus discover_more_tools and your own extra_always_on names.
How many tools one call carries#
Each tool definition costs about 1.2k tokens and is sent again on every LLM round, so the list is bounded:
| Limit | Default | Means |
|---|---|---|
max_total_tools |
35 | The most definitions in one call |
max_carried_tools |
5 | Tools kept from earlier turns, most recently used first |
max_mid_turn_tools |
5 | Tools discover_more_tools may add in one turn |
max_tools on ToolDiscovery |
12 | Tools discovery returns for one message |
Always-on tools and the tools discovered for the current message are never dropped, so a call can pass max_total_tools when those alone are over it (the SDK logs one warning). Pass None to remove a limit. The limits apply only when discovery is on.
6. Memory: what the agent reads and writes#
Each fabric is a graph of concepts with facts (attribute and value; a new value supersedes the old one), timeline events and weighted, typed edges.
- Write with
ingest()(concepts with nested facts and timeline) orlearn()(one concept). Both return what the engine did: the label as the engine resolved it, the identity status, the facts set and superseded. An identity merge can land a write on a different existing label, so use the returned label. - Recall with
think(): spreading activation from a concept or keywords. Alsoquery()(semantic search, facts and timeline),facts(),timeline()andlist_concepts(). - Typed edges.
link(a, b, edge_type="IS_A")asserts a relation (anchors, seed bridges).delete_edge()is a soft delete: the edge leavesgraph()butthink(as_of=...)can still reconstruct it. - Identity. The engine merges duplicate concepts, or files a conflict when it cannot decide (only when it has an LLM configured). The SDK surfaces it:
recent_similarhints on writes, and the conflict tools above. - Point in time.
think(as_of="2026-09-15")rebuilds memory as it stood then.as_ofis the moment a memory was stored, not a date inside what it says. If a meeting from March was entered in May, askingas_ofa March date returns nothing, because nothing existed yet.memory_thinktells the model this. - Retrieval plans.
think(plan="exact > spread(2) > reserve(0.4)")picks the strategy;ThinkResult.planreports what ran. Thememory_thinktool does not exposeplan.
Limits to design around: facts() returns the current value and one previous value, not the full history of an attribute; there is no route to delete a single concept; the engine's identity log lives in its memory and is lost on restart; every fabric stays loaded in the engine, so memory grows with the number of fabrics.
7. Seeding#
A seed gives a new fabric its first knowledge so the agent does not start with an empty graph. A Cradle project writes it as Python modules (agent/seed/concepts.py, intents.py, bridges.py, workflows.py, rules.py, anchors.py) and loads it with scripts/seed_load.py, at registration and again at login (which also recreates a deleted fabric). The parts:
- Concepts: the domain vocabulary (
DOMAINtype by default), with facts, aliases and tags. - Intents: phrases users say, linked to the tools that serve them.
- Bridges: concept to tool links, so a question about a topic finds the right tool.
- Workflows: chains of tools that go together (
PRECEDESedges). - Rules: starter behavioural guidance with
whenandthen.
Rules to follow:
- Index the tools first. A link to a tool name that does not exist yet makes the engine create a plain concept with no type, and discovery only finds
TOOLconcepts. Runawait discovery.index_tools()before loading a seed that names tools. (load_seed(hippo, "seed.yaml")also creates theTOOLconcepts it needs, but a hand-written seed script does not.) - Names must match exactly. A misspelled tool name does not raise an error; the edge points at a tool that is not registered.
- A concept label that contains a tool name (a concept called
manage_task notes) captures the links meant for the tool. Keep tool names unique inside labels. - Seed once.
should_seed(hippo)judges from the fabric statistics whether it looks unseeded; the loader is meant for a fresh fabric.
The YAML seed format and design guidance are in Pre-Education.
8. A chat turn#
reply = await agent.chat(text, conversation_id=thread_id, reasoning_effort=effort)
- History. The agent keeps one history per
conversation_id, each turn with its full tool trace, up tohistory_windowturns (default 5). Always pass your own conversation or user id: without one, every caller shares one history. Some projects control history themselves instead: they setagent.history = list(prior)before each turn, with the last few exchanges of the current conversation, and keep their own setting for how many exchanges to include. That still works in 2.0.0: the list is stored as one turn in the default conversation, replacing what was there, sohistory_windowhardly matters in that mode. - Why full traces matter. A history that shows only the final prose teaches the model to imitate prose and stop calling tools. Keep the trace, and keep the window small.
- Rounds. One turn runs up to
max_rounds(20) LLM rounds. At the limitchat()returns a fixed "max tool rounds exceeded" message andlast_turn.statusismax_rounds. - Images.
chat_with_image(image_b64, media_type, text="", conversation_id=...)sends an image through the same loop; the image is replaced by a short note in history afterwards. - How it ended.
agent.last_turnhasstatus(completed,stopped,max_rounds,cancelled,failed),rounds,tools_used,duration_ms,errorandreasoning_effort. - Timeouts. Two levels, and both are worth setting.
BaseAgent(timeout=45)bounds one LLM call (withModelRouter(..., timeout=45)when routing), so a stalled round fails in 45 seconds instead of LiteLLM's 600. Wrapping the wholechat()inasyncio.wait_for(..., timeout=...)bounds all rounds and the tool time between them. This replaces the olderlitellm.request_timeoutglobal.
9. Streaming to the chat UI#
Turn streaming on in the agent, attach a queue, and drain it while chat() runs:
agent = BaseAgent(..., enable_streaming=True)
queue = asyncio.Queue()
agent.event_queue = queue
task = asyncio.create_task(agent.chat(text, conversation_id=thread_id))
while True:
getter = asyncio.ensure_future(queue.get())
done, _ = await asyncio.wait({getter, task}, return_when=asyncio.FIRST_COMPLETED)
if getter not in done: # the turn ended without another event
getter.cancel()
break
event = getter.result()
... # forward to the browser as an SSE item
if event["type"] == "turn_end":
break
reply = await task # also raises anything the turn raised
| Event | Fields | Use it to |
|---|---|---|
round_start |
round, max_rounds, tools_available |
show "thinking" |
text_delta |
round, delta |
append text as it arrives |
llm_response |
round, latency_ms, tokens_in, tokens_out, cached_tokens, tool_calls, stop_reason |
log usage |
stream_reset |
round |
discard the text collected for that round: it failed and runs again without streaming |
tool_start, tool_result |
tool, status, latency_ms, is_error, round |
show "working on..." lines |
response |
content, turn_id, rounds, tools_used |
the complete answer (only when the turn completes) |
turn_end |
status, turn_id, rounds, duration_ms |
close the stream, always last |
Streaming needs both the flag and a queue; without either the agent uses the plain call. Events carry no conversation id, so run one turn at a time per agent. Detach the queue (agent.event_queue = None) when you stop reading. There is no error event: errors come out of chat().
Stop and disconnect#
- Stop button:
agent.stop(conversation_id)ends the running turn gracefully. It aborts the in-flight LLM call, lets a running tool finish, skips the rest, saves the turn so far with a note for each tool call that did not run, and makeschat()return the text produced so far (last_turn.status == "stopped"). - Client disconnects: cancelling the task that runs
chat()now cancels the LLM call too, for every provider. The turn ends with statuscancelled, and a turn that had already run tools stays in history so the next turn does not repeat the writes. Tools markedatomic = Truefinish first. - Before the next turn, wait for the previous task to finish, or your lock will let two turns overlap.
Details: Events and Streaming.
10. Reasoning effort in the UI#
A reasoning model can think before it answers: slower and more expensive, better on hard questions. To offer it in the chat UI:
- Ask once whether to show the control:
agent.supports_reasoning_effort()(orhippodb_agent.core.llm.supports_reasoning_effort(model)before an agent exists). It follows LiteLLM's model table, so a brand-new model may report false until LiteLLM is upgraded. - Let the user pick Low, Medium, High or Off, and send it with the message. The chat request model (
ChatStreamRequestinagent/routes/chat_routes.py) getsreasoning_effort: Literal["low", "medium", "high", "off"] | None = None, and your handler carries it to the agent (an extra parameter onrespond(), or a field on the per-turn context object if your project has one). - Pass it on:
agent.chat(text, reasoning_effort=effort).Noneuses the agent's default (BaseAgent(reasoning_effort=...), from yourLLM_REASONING_EFFORTsetting);"off"turns the default off for this message. Any other value raisesValueErrorbefore the turn starts.
What to know: the SDK adds room for the thinking to the output limit and keeps Claude's signed thinking blocks with the tool calls of the turn, so it works in tool loops and with streaming. A model that cannot reason ignores the setting with one warning. The newest Claude models decide for themselves whether to think, so an easy question may show no thinking. On Anthropic, changing the effort between messages makes that one message re-read the conversation uncached.
11. Models, keys and errors#
- Model and key.
BaseAgent(model="provider/model", api_key=...). Use the LiteLLM form (anthropic/claude-sonnet-4-5-20250929,openai/gpt-5,gemini/...). Pass the key; a key that only lives in.envis not in the environment. - Routing.
ModelRouter(routing_models={model: when_to_use}, router_model=...)lets a small model choose a model for each message. A message routed to a model without reasoning support ignores the effort setting. - Retries and caching. The SDK retries rate limits, overload and connection errors up to twice with backoff, honouring the provider's
Retry-After. It marks prompt-cache points for Anthropic models. To change either, buildLlmClient(max_retries=..., prompt_caching=...)and pass it asllm=.
What chat() can raise#
| You catch | Meaning | Reasonable reply |
|---|---|---|
LlmRateLimitError, LlmOverloadedError, LlmConnectionError |
The provider is busy or unreachable (retryable is true; retry_after may say how long) |
"The model is busy, please try again" |
LlmTimeoutError |
No answer in time | "That took too long, please try again" |
LlmAuthError |
The LLM key was rejected | Alert an operator; not the user's problem |
LlmContextWindowError |
The conversation is too long for the model | Start a new conversation, or shorten the history |
LlmBadRequestError |
The provider refused the request (parameters, unknown model, blocked content) | A generic failure; log it |
FabricNotFoundError |
The user's fabric is gone | Re-seed and retry once (section 3) |
httpx.HTTPStatusError 401 or 403 from HippoDb |
The HippoDb key is missing, bad or lacks a scope | Alert an operator |
All the Llm...Error classes derive from LlmError (and from HippoDbError); each has model, provider, status_code, retry_after and retryable, and the LiteLLM exception is its __cause__. agent.last_turn.status is failed after any of them.
12. Behaviour worth knowing#
memory_thinkhides the SDK's own scaffolding.TOOL,INTENTandWORKFLOWconcepts share the fabric with the user's memory and used to leak into recall results. They are filtered out unless the model asks for those types by name.- A tool error is never an empty string. A failure with no message (a timeout) reaches the model with the exception's type.
- A turn that is cut short is remembered. The model does not repeat the writes of a stopped or cancelled turn.
- Tools are sent in a fixed order, which helps provider-side prompt caching. Within one turn the list changes only when
discover_more_toolsadds tools; across turns it changes with discovery. discover_more_toolsadds at most 5 tools per turn, and adds nothing (instead of everything) when nothing matches.- Hard rules and the data gateway are available, but only if you wire them. Rules the model saves with
memory_learn_ruleare soft guidance. Enforcement in code (RuleEnforcer) and PII masking (DataGateway) have to be added to the registry by the host. See Compliance and the host-integration guide.
13. Settings: your Cradle Settings to the SDK#
The SDK does not read your .env. Your Settings reads it, and the handler passes the values on.
Already in the Cradle starter's Settings:
| Setting (env) | Goes to | Default |
|---|---|---|
HIPPODB_URL, HIPPODB_API_KEY |
HippoClient(base_url=, api_key=) |
manager URL, none |
MODEL |
BaseAgent(model=) |
the starter's default model |
ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GOOGLE_API_KEY, GEMINI_API_KEY, COHERE_API_KEY, MISTRAL_API_KEY |
BaseAgent(api_key=) for the key of the chosen model's provider |
none |
LLM_ROUTING_MODELS, LLM_ROUTER_MODEL |
ModelRouter(routing_models=, router_model=) |
off |
ENABLE_STREAMING |
BaseAgent(enable_streaming=) |
off |
Add these to your Settings and pass them on if you want them (the SDK defaults apply until you do):
| Setting (env) | Goes to | SDK default |
|---|---|---|
LLM_CALL_TIMEOUT_S |
BaseAgent(timeout=), ModelRouter(timeout=) |
none (LiteLLM's 600 s) |
LLM_REASONING_EFFORT |
BaseAgent(reasoning_effort=) |
none |
MAX_TOTAL_TOOLS, MAX_CARRIED_TOOLS, MAX_MID_TURN_TOOLS, TOOL_LIMITS_ENABLED |
BaseAgent(max_total_tools=, max_carried_tools=, max_mid_turn_tools=); None removes a limit |
35, 5, 5, on |
HISTORY_WINDOW |
BaseAgent(history_window=) |
5 |
MAX_ROUNDS |
BaseAgent(max_rounds=) |
20 |
DISCOVERY_MAX_TOOLS, DISCOVERY_MIN_ACTIVATION |
ToolDiscovery(max_tools=, min_activation=) |
12, 0.05 |
| a turn timeout and a session-build timeout | your own asyncio.wait_for around chat() and the agent build |
yours |
how many past exchanges go in agent.history |
your own handler | yours |
Anything your project does not forward keeps the SDK default. The full list is in the technical reference, section 17.
14. Seeing what the agent did#
agent.last_turnafter each turn: status, rounds, tools used, duration, error, reasoning effort. Also the log lineCHAT TURN n END status=...(loggersdk.agent).- Memory traffic. Every think, query, ingest and link is one INFO line on the
sdk.memorylogger, tagged with the turn and fabric. The full request and response bodies are DEBUG records. A host that wants them in a file callssetup_memory_logging(log_dir, include_payloads=True)fromhippodb_agent.logging.setup. - HippoDb errors. The
memory.clientlogger writes a WARNING with the response body for every status of 400 or more. That is where a 401 reason or a 404 on a lost fabric shows up. - Tool discovery.
sdk.discoverylogs which tools it found, which phase found them, and the index summary.discovery.last_index_reportlists tools learned, repaired, refreshed, skipped or in conflict. - A health report for a fabric.
python -m hippodb_agent.monitor.brain_report --fabric <name> --api-key ...(or withHIPPODB_API_KEYset) prints concept counts by type, the most and least activated concepts, edge strength, orphans and fact coverage.
15. Upgrading a Cradle project from 1.x#
- Bump the pin (
uv add hippodb-agent-sdk==2.0.0) and run the test suite. A search of a wired project found nothing it uses that 2.0.0 removed. - Pass the HippoDb key at every
HippoClient(...)through one helper (section 3). - Remove the once-per-turn
index_tools()call from the chat handler if you like;discover()does it now. - Replace the
litellm.request_timeoutglobal withBaseAgent(timeout=...), and update the test that asserts the global. - Handle
LlmErrorwhere you used to catch LiteLLM exceptions. Code that caughtlitellm.RateLimitErrorno longer sees it. - Expect a smaller history window default (5, was 20) if you rely on
conversation_idhistory. A handler that setsagent.historyitself is not affected. - Add the new features you want: reasoning effort (section 10), a stop button (section 9), tool limits (section 5).
- Update the stale
>=1.2.4notes in comments and inSDK-LINKAGE.md, and the "SDK defaults.env" line in.env.example.
The complete list, including each removed call, is in the release notes.
16. Quick troubleshooting#
| Symptom | Likely cause and fix |
|---|---|
| Registration or the first chat returns 401 | The HippoDb key is not reaching the client. Build every client through the helper and pass api_key= (section 3) |
| 403 on creating a fabric | The key lacks fabric:write, or the fabric cap (max_fabrics) is reached |
| The model never offers one of my tools | tool_scopes has no prefix that matches it, the tool is not indexed (index_tools()), or it is not always-on and discovery did not pick it. Check discovery.last_index_report and the sdk.discovery log |
| A tool is offered but the model ignores it | Its description is vague. Add keywords and examples, and say when to use it |
| The agent forgets earlier messages | chat_history_window (or HISTORY_WINDOW) is small; older facts must come from memory, so check that memory_remember is being called |
memory_think finds nothing for a past date |
as_of is the time a memory was stored, not an event date in its text (section 6) |
| The agent writes "task created" but nothing happened | The model saw a history without tool traces. Keep full traces in history and do not rewrite them to prose |
FabricNotFoundError after a restart or an ops action |
The fabric was deleted. Re-seed and retry once (section 3) |
| A turn hangs | No timeout set. Use BaseAgent(timeout=) and wrap chat() in asyncio.wait_for |
| Reasoning effort has no effect | The model is not a reasoning model in LiteLLM's table (one warning is logged), or it is an adaptive model that chose not to think |
{manifest} renders empty |
manifest_fn was not passed, or refresh_manifest() was never called (section 4) |
| Two users see each other's tools or events | One BaseAgent is serving concurrent turns. Serialise turns or use one agent per user |
17. Reference documents#
- Building an Agent Without AgentRunner: the tested wiring example
- Technical Reference: every class, method and setting
- LLM Routing: models, keys, routing, retries, typed errors, caching, reasoning effort
- Events and Streaming: the event queue, stop and cancel
- Pre-Education: seed design
- Compliance: the data gateway and rules, if you add them
- Release notes 2.0.0: what changed and how to upgrade