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.
  • The platformctl CLI, installed and signed in. See Install the CLI.
  • 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.

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
Signing in is for managing, not for talking

Deploying, reading logs, setting secrets, and memorizing all require a signed-in account. Simply invoking an agent does not — the data plane is open by default. That asymmetry is deliberate and it comes up again in Act 4.

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. Create it:

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

Four lines carry all the weight:

  • 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. This is the ADK convention; the other supported frameworks have their own entry symbols (see 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 — a built-in tool. When the model calls it, your snippet runs in a throwaway code sandbox pod that is destroyed afterward and never reused.
  • search_memory — the other built-in tool. It 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. The base image the platform builds on already ships ADK 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
status: -> building
status: building -> deploying
status: deploying -> ready
research-buddy is ready at http://research-buddy.cai-p-ab12cd.svc.cluster.local

What happened: the CLI packed the folder into a .tar.gz, uploaded it, and the platform built a container image from your code on top of the managed agent harness, then started it as a serverless service. The framework was auto-detected as ADK because the folder contains agent.py. The CLI polls every 2 seconds and gives up after 5 minutes.

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.

Confirm the deployment:

platformctl status research-buddy

You should see a field/value table with status ready, plus name, framework, url (the agent's internal in-cluster address, not a public URL), image, and created_at / updated_at — and message if the API set one.

platformctl status does not show a public URL or the latest revision name. Its output struct (apiclient.Agent) carries only one URL field, the internal address, and has no field for external_url, public_url, or latest_revision, even though the server returns them. To see the public URL and the latest revision name, call GET /v1/agents/research-buddy directly or open the agent in the console.

At this point you have: a live, scale-to-zero endpoint running your agent. If the status 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:

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, holds none of your agent's credentials, and cannot reach the network except for 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. If you invoke over raw HTTP without signing in and supply your own session_id, it must be at least 24 characters of unguessable randomness, or you get a 400 explaining that a short, guessable id would let anyone else read the conversation. Signed-in callers are exempt. 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

Memorizing takes the session you built in Act 3 and turns it into durable, searchable knowledge:

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

You should see:

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

Under the hood the platform flattened the session's turns into text, turned that text into an embedding (a list of numbers that captures its meaning), and stored it in a vector collection that belongs to this agent. Nothing is memorized automatically — you choose which conversations become durable knowledge.

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

Memorizing always requires authentication, and memory is shared

Writing to the memory bank requires a signed-in caller even though invoking does not. The reason is in the platform's own error message: stored memories are read back into later callers' context. There is no per-caller partition — anything you memorize can be recalled by every future conversation with that agent. Never memorize something one user told you that another user should not see.

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

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 — no --session flag — and ask about the boat:

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, the conversation history is empty, and the answer is still right — because 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 live pods: it is scaled to zero (no running replicas), 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

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

Now invoke once more:

platformctl invoke research-buddy "Are you awake?"

The first reply after an idle period is slower — the platform has to start a machine before your message can be answered. That 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.


Optional: the same thing over plain HTTP

Nothing here is CLI-specific. Invoking is an ordinary HTTPS request, which is how you would call the agent from an app.

There is no public API hostname in this alpha, so set CAI_API to the endpoint your administrator gives you (or let platformctl port-forward for you and use that address):

export CAI_API=http://localhost:8080

curl -s -X POST "$CAI_API/v1/agents/research-buddy/invoke" \
-H 'content-type: application/json' \
-d '{"message":"Please compute 2**32 in python."}'

You should see:

{"session_id":"...","user_id":"...","output":"2**32 is 4294967296.","reasoning":"","tool_calls":[{"name":"run_python","summary":"called with args={'code': 'print(2**32)'}"}],"events":[...]}

Note that this call carried no credential at all — the data plane is open by default. The memorize step is the opposite: it needs Authorization: Bearer <token>, and asking for "memorize": true on an anonymous invoke is refused with a 401 that tells you exactly what to do instead. See API authentication.

For a live, token-by-token response instead of one JSON blob, POST the same body to /v1/agents/research-buddy/invoke/stream — details in invoke.


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

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 not reversible, and the memory bank has no separate management surface in this alpha — you cannot list, browse, or selectively delete individual memories through the console or CLI. Write (memorize) and read (search_memory) are the operations that exist today.


If something breaks

SymptomCause and fix
Status is failed right after deployThe build or the agent's startup failed. The failure output is in the agent's status messageplatformctl status research-buddy shows it, and in the console it is rendered in full on the agent's Overview tab. 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. Invoking works without it; deploying, logs, secrets, and memorize do not.
--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 Engine 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 planeTalking to an agent is open by default; changing it always needs a sign-in.

Next steps