Memory and files
Give an agent a memory it keeps across conversations, share memories between agents, and give it files it works on in each one.
An agent remembers in two ways. Its memory is a set of named notes it keeps across every conversation: your refund policy, how a customer likes to be addressed, what yesterday's run already covered. Its workspace holds the files it works on in one conversation: a report it wrote, a spreadsheet you gave it. Your code reads and writes both.
How memory works
Memory is a set of entries, each with a name, a one-line description and Markdown content. At the start of every turn the agent sees each entry's name and description; it reads a body when it needs one, and saves what it learns as it goes, with memory tools it always has (memory__search, memory__read, memory__save and memory__delete; your policy calls them memory.search and so on). So the description is what makes an entry useful: it's how the agent knows the entry is there and what it's for.
Ask the agent to remember something, a code, a number or a name, and it saves it word for word before it answers. Any other secret, like a key a tool prints, it doesn't save.
Seed what it should know
import noclick
support = noclick.Agent.from_name("support")
support.memory.save(
"refunds",
"Refund within 30 days of purchase, to the original card. Gift cards are never refunded.",
description="Our refund rules",
)save creates the entry, or replaces the one with that name. A name is 1 to 64 lowercase letters, digits and . _ -; a description is at most 200 characters, and content 1 to 20,000. A new entry needs a description; saving an existing one without description keeps the one it has.
Read what it remembers
support = noclick.Agent.from_name("support")
for entry in support.memory.list():
print(entry.name, entry.description, entry.updated_by, entry.updated_at)
refunds = support.memory.read("refunds")
print(refunds.version, refunds.content)
for entry in support.memory.search("refund policy"):
print(entry.name, entry.content)list gives each entry without its content; read loads one in full. search looks where the agent looks and finds entries by their words: an entry matches when its name, description or content holds any word of the query (a word also finds the longer words it starts, so refund finds refunds), or its name or description holds the whole query. The more words an entry holds, the higher it ranks, and the name and description count more than the content. It returns the best 10 (limit= up to 100), with content. updated_by says who last changed an entry: agent, api or console.
Correct an entry safely
The agent may change an entry between your read and your write. Pass the version you read and the save only lands if nothing changed since:
support = noclick.Agent.from_name("support")
entry = support.memory.read("refunds")
try:
support.memory.save("refunds", entry.content + "\nNo refunds on custom orders.", version=entry.version)
except noclick.Conflict:
print("The agent changed it meanwhile; read it again.")Without version, a save replaces whatever is there. delete takes a version the same way:
support = noclick.Agent.from_name("support")
entry = support.memory.read("old-pricing")
support.memory.delete("old-pricing", version=entry.version)Whose memory it is
A user's own agent has one memory, the user's. An account agent, shared by every user who messages it, keeps two layers:
- The account's. Entries saved outside
as_user. Every user's conversations read them; besides your code and the console, only the agent's conversations with your account itself (messages your code sends outsideas_user) change them. - Each user's. What the agent learns while talking to a user, and what you save for that user. No other user's conversations see it. In a user's conversation the agent reads the account's entries too, and a save of the same name makes the user's own copy.
support = noclick.Agent.from_name("support")
support.memory.save("tone", "Warm, brief, no jargon.", description="How we write") # every user
support.memory.save("plan", "Ana is on the Team plan, billed yearly.", description="Her plan", user="user_42")
print([entry.name for entry in support.memory.list(user="user_42")]) # ['plan']: one layer at a timelist, read, save and delete act on one layer: the account's outside as_user, the user's inside. search looks where the agent looks, in the user's entries and the account's; each result's user_id says whose it is (None for the account's). A layer's entries go when the agent is deleted.
When the agent answers someone outside your account, its memory is read-only: an inbox's sender, a caller on a phone number or a visitor to its share link can't make it write anything every user would then read. A test run reads memory and never writes it.
Share a memory between agents
A memory is an object of its own, with a name, that any number of agents can read: what every agent of your company should know, or what one agent learned that another needs. Attach it with memories=, and the agent reads it besides its own memory:
company = noclick.Memory.from_name("company", create_if_missing=True)
company.save("tone", "Warm, brief, no jargon.", description="How we write")
support = noclick.Agent.from_name("support", create_if_missing=True, instructions="Answer billing questions.",
memories=[company])
scribe = noclick.Agent.from_name("scribe", create_if_missing=True, instructions="Keep the company's notes.",
memories=[company.writable()])An attached memory is read-only to the agent unless it's attached with writable(); its own memory, agent.memory, it always writes. Every turn starts with the name and description of each entry it can read, its own memory's first and then each attached one under its name. Its memory tools take memory="company" to read or write a particular one; without it, memory__save and memory__delete act on its own memory, memory__read looks in its own memory first and then in the attached ones (naming them when two hold the same entry), and memory__search looks in all of them.
Every agent's own memory is a memory too: agent.memory_id is its id, and memories=[other.memory] lets an agent read what another one remembers. It goes when its agent is deleted for good; a named memory outlives the agents that attach it.
A memory belongs to your account or to one user, as a volume does. The account's keeps its own entries and each user's, as an account agent's own memory does; a user's keeps one layer, its user's. An agent attaches memories of its own scope, and a user's agent the account's too (read-only: only a call acting for a memory's own scope attaches it with writable()), never another user's; a browser session never attaches the account's. Up to 10 memories besides its own.
for memory in noclick.Memory.list():
print(memory.name, memory.entry_count, memory.attached_to)
company.entries() # one layer, as agent.memory.list() is
company.read("tone")
company.forget("old-pricing")
company.delete() # to the trash, once no agent attaches itA memory's entries are read and written exactly as an agent's own memory's are: the same names, descriptions, versions and layers (entries, search, read, save and forget, with user= for a user's layer). Deleting one moves it to the trash with its entries, and is refused while an agent attaches it (noclick.Conflict with attached_to): take it out of the agents' memories first.
Long conversations
A conversation that runs for weeks doesn't run out of room. Once an sdk agent's conversation grows past about 32,000 tokens, its turn folds the older part into a checkpoint: one entry of the agent's memory, named context. and a hash, in that conversation's layer, with conversation set to its key. Each later turn reads the checkpoint and then the recent messages in full, and the conversation you read back stays whole. The checkpoint keeps word for word anything the agent was asked to remember in that conversation, until it's asked to forget it, and leaves out any other secret it saw.
support = noclick.Agent.from_name("support")
for entry in support.memory.list(user="user_42"):
if entry.conversation == "ticket-1841":
print(support.memory.read(entry.name).content) # what the agent carries forwardA checkpoint is yours to read and correct like any entry: an edit wins over the next summary, and deleting it forgets that part of the conversation for good, since it's never rebuilt from the history. The agent sees it only in its own conversation, never in its list of entries, memory__search or its memory tools, and it doesn't count toward a layer's entries. Names starting with context. are kept for checkpoints. A conversation whose memory is read-only keeps none. Summaries run on NoClick's default model and are billed with the turn. Agents on a coding harness compact their own conversations.
Work with a conversation's files
Every conversation has a workspace, a folder the agent sees as /workspace in each turn of that conversation. What it writes with its shell or its tools lands there, and what you put there is waiting for it (a workspace of channels is something else):
analyst = noclick.Agent.from_name("analyst")
workspace = analyst.workspace(conversation="q3-review")
workspace.write("sales.csv", "region,total\nnorth,1200\nsouth,950\n")
reply = analyst.message("Chart sales.csv by region and save it as chart.png.", conversation="q3-review")
for file in workspace.files():
print(file.path, file.size_bytes, file.modified_at, file.url)workspace() sends nothing; it names a conversation (of the current user, or of user=): each user's thread with an account agent has a workspace of its own. files lists what's there, each with a download url that works without a key for about an hour. read returns a file's bytes, and url makes a link for your user to click:
analyst = noclick.Agent.from_name("analyst")
workspace = analyst.workspace(conversation="q3-review")
chart = workspace.read("chart.png")
link = workspace.url("chart.png", expires_in_s=600)A link lasts an hour unless you say otherwise, and a day at most.
An sdk agent's shell starts each turn from the workspace as it stands, so a file you write reaches its next turn. An agent on a coding harness keeps its sandbox up between turns and mounted the workspace when the sandbox started, so it sees your file once the sandbox starts again, a few minutes after its last turn: write before a conversation's first turn, or between its sessions. A volume is the place for files every conversation shares.
Limits and billing
- A memory holds up to 500 entries per layer, and an agent attaches up to 10 besides its own. Memory is free to keep; the agent's own reads and saves are part of its turns.
- Each turn's list of names and descriptions takes up to about 6,000 characters of the turn, for all its memories together: its own first, the account's entries after the user's and newest first in each; past that the agent finds the rest with
memory__search. - A file you write to a workspace is at most 100 MB. A workspace lasts as long as its agent, and goes when the agent is deleted for good. Workspaces cost nothing to keep.
Errors
| Error | When |
|---|---|
noclick.NotFound | No entry by that name in that layer, no file at that path, or no memory by that name or id. |
noclick.Conflict | The entry isn't at the version you passed any more (details["current_version"] is where it is, absent once it was deleted); a memory name that's taken (existing_id); deleting a memory an agent attaches (attached_to) or an agent's own memory. |
noclick.InvalidRequest | A name that isn't allowed, a new entry without a description, a description over 200 characters, empty content or content over 20,000 characters, an agent's own memory among its memories, or one memory listed twice. |
noclick.PermissionDenied | A user's memory attached to an agent outside its user, a memory attached writable() by a call that doesn't act for its scope, or the account's memory from a browser session. |
noclick.QuotaExceeded | The layer already has 500 entries, or more than 10 memories in memories. |
Every method is in the reference: AgentMemory with save and search, Memory with writable, MemoryEntry, and AgentWorkspace with files.