SDK
Sessions and files
A conversation, read from where the agent wrote it, and the files that travel with it. sessions wraps the sessions API and artifacts the artifacts API.
Conversations
A session is ADK's own, stored in your agent's database, and these methods read it through ADK's session service — so a transcript is exactly what the agent saw, and it does not depend on the run history being available. Sessions and runs explains how a conversation and its runs relate.
from elan_sdk import Elan
elan = Elan.from_env() # every call uses the client's project unless you pass project=
session = elan.sessions.create(
"hotel-concierge",
session_id="sess-ada-1",
user="ada",
state={"plan": "pro"},
title="Late checkout for room 412", # what a list shows; the agent never sees it
)
for s in elan.sessions.list("hotel-concierge", user="ada"): # newest first
print(s.id, s.title, s.state_keys)
one = elan.sessions.get("hotel-concierge", "sess-ada-1", user="ada")
print(one.state)
elan.sessions.delete("hotel-concierge", "sess-ada-1", user="ada")| Method | What it does |
|---|---|
| create(target, *, session_id, state, title, user, project) | Starts a conversation. The id is yours to choose, or ADK mints one; an id already in use is a Conflict, never a silent reuse. |
| list(target, *, user, project) | The conversations with one agent, newest first. Each carries state_keys rather than its whole state. |
| get(target, session_id, *, user, project) | One conversation's id, title, state and last update, without its events. |
| events(target, session_id, *, user, recent, after, project) | Everything in the conversation, as the events the live stream sends. |
| turns(target, session_id, *, user, project) | The messages alone: each person's and each agent reply's text, built from events(). |
| run(target, session_id, *, user, project) | A workflow run's inputs, steps and pause, as a plain dictionary shaped for a UI rather than a typed model. |
| delete(target, session_id, *, user, project) | The conversation and every event in it. Not recoverable. |
TypeScript has the same seven methods, with everything after the target and session id in an options object: sessions.events(target, id, { user, recent, after }).
- A conversation lives in a project, so every method needs one: the client's own, or
projecton the call. With neither, the call fails before any request — aValueErrorin Python, a plainErrorin TypeScript. useris one of your end users, by your own id, and needs a tenant admin credential; without it every method reads the caller's own conversations, which for a service user is usually none. Create and invoke for the same person: a conversation you create as yourself is not the one anend_user_idinvocation runs in.- Starting
statetakes conversation keys only.app:,user:,temp:andagentkit:keys are refused with aBadRequestnaming the key. To change state later, sendstate_deltawith a turn — see Runtime. - A
Conflictsaying “redeploy it once” means exactly that. The agent was deployed before its history could be read; one redeploy makes it readable and changes nothing else. An agent that is not deployed in the project is aNotFound.
Reading history
events() returns the same AgentEvent objects the stream delivers, so the code that renders a live run renders a reopened one unchanged. A person's message is a started event; each run ends with a finished, or a paused that ends_the_run recognizes, so a fold ends each turn the way it does live; and a view the agent showed comes back as a surface event where it appeared.
events = elan.sessions.events("hotel-concierge", "sess-ada-1",
user="ada", recent=200)
for event in events:
print(event.invocation_id, event.kind, event.text)
for turn in elan.sessions.turns("hotel-concierge", "sess-ada-1", user="ada"):
print(turn.role, turn.text)
steps = elan.sessions.run("event-planner", "run-5f2a8c1d") # a workflow's steps and pause- History is not the whole stream.
typing,queuedandaction_requestedare only ever live, so they are not in it, andsegmentis empty. recentandafterpage a long conversation.recentkeeps the newest N, from 1 to 10,000;afterkeeps events at or after a time in seconds since the epoch — inclusive, so the boundary event comes back again.seqis numbered per run within each response, so drop repeats byemitted_atrather than byseq.- How one run ended and what it cost is a different question, answered by
runs.get()and usage.
Files
A file's bytes never pass through the platform. You ask for one transfer to be authorized; the platform signs a URL for exactly that object, size and content type; the bytes go straight between your client and storage. Files and multi-modal covers how an agent then reads one.
from pathlib import Path
ticket = elan.artifacts.upload(
"hotel-concierge",
session_id="sess-ada-1",
filename="invoice.pdf",
data=Path("invoice.pdf"),
content_type="application/pdf",
user="ada",
)
print(ticket.filename, ticket.version)
names = elan.artifacts.list("hotel-concierge", session_id="sess-ada-1", user="ada")
url = elan.artifacts.download_url("hotel-concierge", session_id="sess-ada-1",
filename="invoice.pdf", user="ada")
elan.artifacts.delete("hotel-concierge", filename="invoice.pdf",
session_id="sess-ada-1", user="ada")Four scopes, chosen by the filename
The same prefixes an agent's own load_artifact uses, so a name from one side works unchanged on the other.
| Filename | Scope | Who writes it |
|---|---|---|
| report.pdf | This conversation. | You, the agent. |
| user:prefs.json | This end user, across their conversations. | You, the agent. |
| agent:tone.md | This agent, shared by all of its users. | A tenant admin. Agents only read it. |
| project:policy.md | Every agent in the project. | A tenant admin. Agents only read it. |
The methods
- Python
upload()authorizes and sends in one call, from bytes or aPath, and returns theUploadTicketit used. TypeScript has noupload(): calldeclareUpload()and send the bytes withputBytes(ticket, body), which is a function rather than a method so the client's credential cannot travel with it. declare_upload()is for bytes you do not hold — a file a browser picked. It returns a URL, a method and headers; send the headers exactly as given, because every one of them is signed. The size you declare is the size storage will accept, at most 512 MiB, and the URL lives fifteen minutes. A browser PUT also needs your origin declared underartifacts.originsin elan.yaml, or it fails as an opaque network error.download_url()(downloadUrl) returns a URL that lives five minutes. Omitversionfor the newest;attachment, on by default, makes a browser save it under its real name. A missing file is aNotFound.list()returns every filename the conversation can see, prefixes intact, so a name goes straight back intodownload_url().delete()in Python andremove()in TypeScript remove a file and every version of it. Deleting from a shared scope needs a tenant admin, as writing to one does.
user names one of your end users here too, by your own id. Filenames are checked locally first — an empty name, a second prefix or an escape from the scope is a BadRequest before any request is made — and a deployment with no artifact storage answers NotWired.
Listing only the shared files
The route treats the session id as optional, so that the agent: and project: files can be listed on their own. Both clients make it a required argument of list() anyway. Pass an empty string: both drop an empty parameter rather than sending it.
# An empty session id is dropped from the request, so this lists the
# agent: and project: files only.
shared = elan.artifacts.list("hotel-concierge", session_id="")