Skip to main content

Tutorial: research agent with memory

By the end of this tutorial you will have built, deployed, and torn down Research Buddy — an agent that does two things a plain chatbot cannot:

  1. It computes by writing Python and running it in an isolated sandbox, instead of guessing at arithmetic.
  2. It remembers — not just within one conversation, but across brand-new conversations that share no history at all.

Every command below is copy-paste-runnable, and every command that prints something shows you what to expect. Budget about 20 minutes.

What you are building

Two kinds of memory show up here, and the difference is the whole point of the tutorial:

  • A session is one conversation. The platform stores its turns and replays them to the model on every message, so the agent remembers what you said five minutes ago. When the conversation ends, that context ends with it.
  • The memory bank is long-term storage. You explicitly promote a session into it ("memorize"), and from then on any future conversation with that agent can find those facts by searching.

Before you begin

You need:

  • A platform account and a project. There is no self-service sign-up — ask your administrator for an account or an invitation link. See Create an account.

  • Your project connected to Crusoe Cloud. Everything you deploy is built into a container image, and that image is stored in a repository in your own Crusoe Cloud Registry — so a project with no Crusoe Cloud credential is refused before anything is built. Connecting is a one-time, project-admin step, and a project that is already connected needs nothing new. See connect your Crusoe Cloud account.

  • platformctl tab: the CLI, installed and signed in. See Install the CLI.

  • curl tab: a token and the API address in your shell:

    export CAI_API=https://api.codyhill.dev
    export CAI_TOKEN="<your session token or API key>"
  • Console tab: nothing else. A browser is enough.

  • jq (used to pull the session id out of a JSON response). If you do not have it, you can copy session ids by hand instead.

Every act below is shown three ways — platformctl, curl, and the console. Pick whichever tab suits you and stay in it; your choice follows you across every page in these docs.

Check that you are signed in and pointed at the right project:

platformctl whoami
platformctl projects list

You should see:

you@example.com role=user (credential: cached login (~/Library/Application Support/crusoe-ai/token))
SLUG NAME SHORT ROLE ID
ml-team ML Team ab12cd admin 0f7a...-uuid

Pin the project once so you do not repeat --project on every command:

platformctl config set-project ml-team
Every call below carries a credential

There are two planes here, and on a stock install both are closed. Changing an agent — deploying, reading logs, setting secrets, memorizing — is the management plane, and it always requires a signed-in account. Talking to a deployed agent is the data plane, gated by INVOKE_AUTH_REQUIRED, which ships on. Invoke without Authorization: Bearer and you get a 401 that names the flag rather than an answer. An install can open the data plane deliberately; do not assume yours has. Every curl tab below sends the header. See API authentication.

You do not need a model API key. The platform wires a managed model into every agent it builds.


Act 1: write the agent

Research Buddy is one file. Which file, and what it must define, is the only thing that differs between frameworks — the platform runs all three unmodified, and every act after this one is identical whichever you pick.

mkdir -p research-buddy && cat > research-buddy/agent.py <<'EOF'
from google.adk.agents import Agent

from crusoe_adk.foundry import foundry_model
from crusoe_adk.tools import run_python, search_memory

root_agent = Agent(
name="research_buddy",
model=foundry_model(),
instruction=(
"You are Research Buddy, a research assistant. Use the run_python tool "
"for calculations and the search_memory tool to recall things you have "
"been told to remember."
),
tools=[run_python, search_memory],
)
EOF
  • root_agent — the platform imports your agent.py and looks for a module-level variable with exactly this name. Rename it and the agent will not start. Full contract: ADK.
  • foundry_model() — returns the platform-managed model. Called with no arguments it follows the CHAT_MODEL environment variable, so you can switch models later through configuration instead of editing code.
  • run_python and search_memory come from crusoe_adk.tools.

Two tools do the work in every version, and they are worth naming now:

  • run_python — when the model calls it, your snippet runs in a throwaway code sandbox that is destroyed afterward and never reused.
  • search_memory — searches this agent's own long-term memory bank and returns the top 5 matching snippets. It is empty right now; that is expected.

You do not need a requirements.txt. Each framework's base image already ships the framework, the Crusoe helpers, and these tools. Add one only if your agent imports a library the base image lacks.

At this point you have: a folder with one Python file in it. Nothing has been deployed.


Act 2: deploy it

platformctl deploy ./research-buddy --name research-buddy

You should see:

packaging ./research-buddy...
uploading research-buddy (1.1 KiB, framework=adk)...
build 2f6f1c3a-8b90-4d2e-9f11-6c0a5d7e2b34 accepted
state: -> building
state: building -> deploying
state: deploying -> ready
research-buddy is ready at https://research-buddy-ab12cd.apps.codyhill.dev

The framework on the upload line is whichever one you wrote in Act 1 — the CLI works it out from the entry file in your folder: crew.py means CrewAI, graph.py means LangGraph, anything else means ADK. Force it with --framework adk|crewai|langgraph. While the build runs, the CLI asks for the agent's state every 2 seconds and gives up after 5 minutes.

Confirm the deployment:

platformctl status research-buddy

You should see a table of fields and values including state ready and ready true. The rest are name, framework, the agent's address, image, and created_at / updated_at — plus message if the API set one. Every resource on the platform answers the readiness question the same way: state is the word to show, in this resource's own vocabulary, and ready is the boolean to branch on.

The address shown is the agent's public URL, or private if you have not published it. It is deliberately never the internal hostname, which would look like an address you could call but is not.

The one field platformctl status does not decode is latest_revision. To see revisions, ask for them directly:

platformctl agents revisions research-buddy

They are listed newest first, with the share of traffic each one serves.

Here is what happened. Your folder was packed into a .tar.gz and uploaded. The platform built a container image from your code: a packaged copy of your app plus everything it needs to run. That image sits on top of the managed agent harness, the small web server the platform bakes into every agent. Then the platform started the image as a serverless service.

Everything in the folder is uploaded

There is no ignore file. A stray .venv/ or a directory of sample data ships with your code and counts against the 100 MiB upload cap. Keep the agent folder to just the agent.

At this point you have: a live, scale-to-zero endpoint running your agent. If the state came back failed, jump to If something breaks before continuing.


Act 3: the first conversation — make it compute

Ask it to do arithmetic and tell it a fact you will test later. Capture the session id the platform mints — you need it in Acts 4 and 5.

SESSION=$(platformctl invoke research-buddy \
"My boat is a Mastercraft Maristar 245. Please compute 2**32 in python." \
-o json | jq -r .session_id)
echo "session: $SESSION"

You should see:

session: 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470

Now run the same message without -o json to see it the human way (this starts a different conversation — that is fine, it is just for looking):

platformctl invoke research-buddy "Please compute 2**32 in python."

You should see:

2**32 is 4294967296.
(session: 91b0f4d7-2a6c-4e8f-b3d1-5c7e9a0f2b4d)
tool_call: run_python called with args={'code': 'print(2**32)'}

Three things to notice:

  1. The number is exactly right. The model did not do the arithmetic — it wrote print(2**32), the sandbox executed it, and the real answer came back.
  2. tool_call: run_python is the receipt. Every tool the agent used is listed in the response, so you always know whether an answer came from computation or from the model's own words.
  3. The platform minted a session id and printed it back. Omit --session and you get a brand-new conversation every time.

The sandbox that ran your code was created for that one call and destroyed afterward. It runs as a non-root user and holds none of your agent's credentials. It cannot reach the network either, apart from DNS lookups, so model-written code cannot phone home. See security and limits.

At this point you have: proof that the agent computes rather than guesses, and a session id in $SESSION holding the boat fact.


Act 4: short-term memory — continue the conversation

Send a follow-up into the same session:

platformctl invoke research-buddy "What boat do I have?" --session "$SESSION"

You should see:

You have a Mastercraft Maristar 245.
(session: 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470)

The model was never told the boat's name in this message. The platform stored turn 1 and replayed the whole conversation to the model for turn 2. That is a session doing its job.

Now prove the negative — start a fresh conversation and ask the same question:

platformctl invoke research-buddy "What boat do I have?"

You should see: a reply along the lines of "I don't have that information" or a request for more detail, plus a different session id. Sessions do not leak into each other. Each one starts blank.

That gap is exactly what the memory bank exists to close.

Where session ids come from, and why length matters

Let the platform mint your session ids. Signed-in callers — which is everyone in this tutorial — may name a session anything. The floor applies only where an install has opened the data plane and a caller invokes with no credential: that caller's own session_id must be at least 24 characters of unguessable randomness, and anything shorter gets a 400 explaining why. Whoever names a session owns its history, so a short, guessable id ("s1", "test") would let anyone else read the conversation back. Details in invoke.

At this point you have: seen both halves of short-term memory — continuity inside one session, and a clean slate between sessions.


Act 5: promote the conversation into long-term memory

A new agent's memory is off: it forgets everything between sessions until you decide otherwise. Whether an agent remembers is a policy on the agent - off, auto (distil facts from every conversation) or explicit (only when asked). Turn it on as explicit, since the next step is asking:

platformctl agents memory set research-buddy --mode explicit

You should see:

mode explicit
a new revision is rolling; the running agent serves the new policy once it is ready

Wait for platformctl agents memory status research-buddy to report mode explicit - a policy change rolls a new revision - then memorize. Memorizing reads the session you built in Act 3 and distils it into facts about you, stored per user and searchable in any later conversation:

platformctl memorize research-buddy --session "$SESSION"

You should see:

memorized session 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470 for research-buddy

The --session flag is required. Leaving it off fails with --session is required.

Three things happened under the hood. The agent's own model read the session and wrote short, standalone facts about you ("Owns a Mastercraft Maristar 245."). Each fact was turned into an embedding — a list of numbers that captures its meaning — and stored, tagged with your user id, in the memory collection that belongs to this agent. Under explicit nothing is memorized until someone asks; under auto this happens after every reply.

Two rules worth internalizing now, because both surprise people later:

Memorizing always requires authentication, and memory is per user

Writing memory requires a signed-in caller even though invoking does not: a stored fact is read back into that user's later conversations. Memory is filtered per user - a fact about you is never served to anyone else - so the thing to watch is the other direction: whoever sends the user_id decides whose memory a conversation writes to. You can see and erase what was stored at any time with platformctl agents memory list and forget.

At this point you have: one memory in the bank. The next act reads it back.


Act 6: long-term memory — recall in a brand-new conversation

Start a conversation with no history at all, and ask about the boat:

Leave off the --session flag:

platformctl invoke research-buddy "What do you know about my boat?"

You should see:

You have a Mastercraft Maristar 245.
(session: 7c1e5b93-4f28-4a0d-9e63-2b8a1c4d6f07)
tool_call: search_memory called with args={'query': 'boat'}

Compare that with the identical question in Act 4, which came back empty. The session id is new and the conversation history is empty, yet the answer is right. The model called search_memory, which searched the bank by meaning rather than by keyword and found the memorized snippet.

That is the payoff: an agent that accumulates knowledge instead of forgetting everything the moment a chat window closes.

At this point you have: a working end-to-end memory loop — teach in one session, memorize, recall in any later session.


Act 7: watch it fall asleep and wake up

Leave the agent alone for a few minutes, then look at its logs:

platformctl logs research-buddy

You should see, once it has gone idle:

agent research-buddy has no running instances: it is scaled to zero, which is normal for an idle serverless agent - it cold-starts on the next invoke. For logs from earlier runs, use GET /v1/agents/<name>/logs/history.

That is not an error. An idle agent runs zero machines. To read logs from earlier runs anyway:

platformctl logs research-buddy --history

Now invoke once more:

platformctl invoke research-buddy "Are you awake?"

Persisted history is kept for 14 days by default, in lines of the form <timestamp> <stream> <message>. It survives both scale-to-zero and new revisions, which is why it is the right thing to reach for when you are debugging something that has already finished.

The first reply after an idle period is slower. The platform has to start a machine before your message can be answered. That extra wait is a cold start, and it is the price of not paying for idle capacity. See autoscaling and scale-to-zero.

At this point you have: seen the full lifecycle, including what "serverless" actually feels like.


Clean up

Delete the agent. This removes the running service, its per-agent secrets and environment configuration, and its stored source:

platformctl delete research-buddy

You should see:

deleted research-buddy

Delete is narrower than it looks: it removes the running agent, not the conversations it stored or anything it memorized. See what delete leaves behind before you rely on it to remove data.

If you pinned a default project only for this tutorial, clear it:

platformctl config set-project ""
What deletion does not give you back

Deleting is irreversible. Manage memory programmatically through agent session memory operations (memorize and search_memory).


If something breaks

SymptomCause and fix
State is failed right after deployThe build failed, or the agent failed to start. The failure output lands in the agent's message field. Run platformctl status research-buddy to see it, or open the agent's Overview tab in the console, which renders it in full. There is no separate build-log endpoint.
could not import 'root_agent' from /app/agent/agent.pyYour agent.py does not define a module-level root_agent. Match Act 1 exactly.
invalid agent name (must be a lowercase DNS label)Names are lowercase letters, digits, and hyphens, must start with a letter, and are at most 63 characters.
this is a management endpoint and requires authentication...You are not signed in. Run platformctl login, or send Authorization: Bearer $CAI_TOKEN on the HTTP call.
this platform requires authentication to invoke agents (INVOKE_AUTH_REQUIRED=true)The invoke carried no credential. Send the same header — invoking needs one too, unless your install sets INVOKE_AUTH_REQUIRED=false.
--session is requiredplatformctl memorize needs the session you want to commit.
Turn 2 forgot turn 1You did not pass --session, so each invoke started a new conversation.
A 409 mentioning "exists in more than one project"The same agent name lives in two projects you can see. Add --project <slug>, or ?project=<slug> on an HTTP call.
this project is at its service limitYour project has hit its service quota. Delete an agent or function, or ask an admin to raise the quota. See quotas and audit.
The reply is a model authentication errorNo model key is configured for your install. Ask your administrator, or set your own with platformctl secrets set research-buddy MODEL_API_KEY=... — see secrets and env.

More symptoms and fixes: Agent troubleshooting.

What you learned

IdeaThe one-sentence version
AgentA folder of Python code the platform builds and runs as a scale-to-zero HTTPS endpoint.
ToolA function the model can decide to call; run_python and search_memory are built in.
SessionOne conversation, replayed to the model each turn — short-term memory.
MemorizeThe explicit step that turns a session into long-term, searchable knowledge.
Memory bankPer-agent long-term storage, searched by meaning, shared by every future caller.
Scale-to-zeroIdle agents run zero machines; the next request pays a cold start.
Data vs management planeChanging an agent always needs a sign-in; talking to one does too, unless the install sets INVOKE_AUTH_REQUIRED=false.

Next steps

Go deeper

These advanced guides pick up where the quickstarts stop, each exercising a different slice of the platform:

GuideFramework / language
Multi-step research agentLangGraph
Editorial pipeline with a crewCrewAI
Support agent over your own docsADK
Document ingestion pipelinePython
Webhook fan-out, exactly onceNode.js
Scheduled reconciliation jobGo
Object-store ETL with move-after-readRuby