Self-awareness — Claude Science's own database and SDK
host.query(sql, params=[], limit=None, df=False) runs read-only SQLite against Claude Science's own metadata DB. It is only available via the repl tool (not python/r). Results are automatically scoped to the current project, so SELECT * FROM frames returns only frames in this project. The repl tool is stdlib-only — df=True returns the raw dict there (use json.dump(..., open("handoff/q.json","w")) and load in a python cell if you want pandas).
Dialect and limits
- SQLite. Epoch-milliseconds for all timestamps
(createdat > strftime('%s','now','-1 day')*1000). Booleans are 0/1. JSON columns are TEXT — use jsonextract(col, '$.key'). Recursive CTEs OK.
SELECT / WITH / PRAGMA / EXPLAIN only; one statement per call;
? placeholders with params=[...].
- Scoping. Most tables are transparently filtered to the current
project (and memories to the current user) via CTEs that shadow the real tables — sessionclaims, verificationchecks, and poller_lease are unscoped. You therefore cannot use main.table / temp.table — schema-qualified names are rejected.
- Caps. Default 200 rows (max
limit=1000); cells >2000 chars are
clipped in place with a …[+N chars] marker; total serialized output capped at ~100k chars (truncated=True, truncationreason="totalsize_cap" — narrow your columns). 5-second timeout.
- Schema introspection:
host.query("PRAGMA table_info(frames)") or
host.query("SELECT name, sql FROM sqlite_master WHERE type='table'").
Queryable tables
Session / conversation
frames — one row per agent frame (a root conversation or a delegated sub-agent). The frame you are running in now is one of these rows. Key columns: id, parentframeid, rootframeid, agentname, delegatename, status (processing/completed/failed/cancelled/ awaitinguserresponse/awaitingplanapproval), model, effort, inputtokens, outputtokens, cachereadtokens, cachewritetokens, totalcost, tasksummary, statusdescription, conversationtype, name, projectid, createdat, updatedat, completedat, lastusermessageat, ishidden. JSON columns: inputdata (what started the frame), outputdata (jsonextract(outputdata,'$.response') is the final response text), contextdata (the full serialized runner state — see below), mentionedartifactids, specialistsused.
contextdata is large. It holds the entire runner state under underscore-prefixed keys — notably $.messages (the full conversation array), $.inputtokens / $.outputtokens / $.totalcost (same values as the top-level columns), $.runningchildren, $.planjson, $.compactioncount, $.toolidtoframeid. Selecting it raw will hit the cell cap; use jsonextract/jsonarraylength to read specific keys. For the messages themselves, prefer host.frames(frameid=...) which paginates — messages via SQL will truncate on any non-trivial session.
compactionarchives — pre-compaction message snapshots. frameid, compactionindex, messagecount, tokencount, summary, messages (JSON array), createdat. When a frame's compactioncount > 0, the original messages that were summarized live here.
notifications — parent↔child messages. senderframeid, recipientframeid, rootframeid, notificationtype, payload (JSON), readat, created_at.
projects — id (proj*, not a UUID), name, description, context, userid, uploadsframeid, memoryenabled, createdat, updated_at.
notes — user annotations. projectid, targettype, targetframeid, targetmessageindex, targetartifactid, content.
Artifacts
artifacts — one row per file. id, projectid, rootframeid, frameid, filename, latestversionid, isuserupload, isephemeral, folderid, sortorder, priority, createdat.
artifactversions — one row per saved revision. id, artifactid, versionnumber, frameid, contenttype, sizebytes, checksum, storagepath, extractedcode, codedescription, language, agentname, isintermediate, ischeckpoint, parentversionid, producingcellid (→ executionlog.id), createdat. JSON: lineagemessages, dependencymappings, environmentsnapshot, annotations, cellsources. Join artifacts.latestversionid = artifact_versions.id for size/type.
artifactdependencies — DAG edges. artifactversionid, dependsonversionid, reference_name.
artifactfolders — id, projectid, parentid, name, rootframeid, isconversationfolder, isuseruploadsfolder, sort_order.
contentsnapshots — content-addressed dedup store. hash, content, sizebytes. Referenced by artifactversions.lineagesnapshothash / envsnapshot_hash.
Execution history
executionlog — one row per python/r/bash/repl cell, in order. id, frameid, cellindex (monotonic), kernelid, kernelkind (analysis/operon), condaenv, language, source (exact submitted code), stdout, stderr, exitstatus (ok/error/kerneldied/ cancelled), errorlineno, fileswritten (JSON [{path, sha256}]), created_at. This is the ground-truth record of everything you've run.
hostcalllog — one row per host.* SDK call made inside a cell. id, executionlogid (→ executionlog.id), seq, method (querydb/llm/mcp/listframes/…), argsjson, derivable, datainline, dataref, error, bytes, createdat. Ordered by (executionlog_id, seq).
Compute and verification
computeusage — remote compute jobs. jobid, environment, tiertype (gpu/cpu), provider, frameid, projectid, startedat, endedat (null ⇒ running), expiresat, state, remoteworkdir, submitcellid. JSON: outputspecs, remote_handle.
sessionclaims — falsifiable claims extracted for verification. rootframeid, frameid, stepid, claimtext, entities (JSON), source (agent/haiku_extracted).
verificationchecks — reviewer verdicts. rootframeid, artifactversionid, claimid, claim, verdict (pass/warn/fail/inconclusive), severity, evidence, rebuttal, reviewermodel, reviewerframeid, sourceref (JSON), status (open/resolved/unaddressed), reflag_count.
memories — durable beliefs (user-scoped; may be absent on some builds). id (mem*), body, subjectprojectid, subjectartifactid, subjectversionid, subjectframeid, sourceframeid, origin (extractor/agenttool/user), evidence (stated/observed/inferred), supersededby, lastsurfaced_at.
pollerlease — single-writer guard for compute polling. provider, holder, expiresat.
Denied tables
These are rejected with Table '<name>' is not queryable — use the listed SDK accessor instead.
- Secrets (encrypted at rest, blocked defense-in-depth):
oauth_tokens,
usersecrets, anthropicapikeys, cloudcredentials. → host.credentials.list() for non-secret metadata; .get(name) for the decrypted fields — usable in client libraries, redacted only from printed cell output.
- Agent/skill/connector configuration (enumerating attack surface has no
legitimate raw-SQL use): useragents, agents, customagentprompts, bundledagentsettings, capabilitysettings, customskills, agentskillassignments, custommcpservers, mcpagentassignments, mcptoolgrants, directoryattachments. → host.agents.list() / host.skills.list() / host.agents.list_connectors() (load the customize skill for that API).
- Host filesystem mounts:
hostgrants. → the listhost_grants tool
(present on sandboxed-network builds).
- Compute provider configuration:
compute_providers. → the
listcompute / computedetails tools.
The denylist matches on word boundaries anywhere in the SQL, so a column alias or string literal that happens to equal a denied table name will also be rejected.
Host identity (hostname, workspace/pod name) is intentionally not exposed anywhere in this DB (and on Linux builds the sandbox masks it as well) — to know where you're running, ask the user or use list_compute labels.
Worked examples
All of these run via the repl tool.
# Token and cost accounting across every frame in THIS PROJECT (all
# sessions). Add `WHERE root_frame_id = ?` with the current root's id to
# scope to one session tree. Aggregate server-side so the row cap can't
# undercount.
r = host.query("""
SELECT COUNT(*) AS n_frames,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens,
SUM(cache_read_tokens) AS cache_read_tokens,
SUM(cache_write_tokens) AS cache_write_tokens,
SUM(total_cost) AS total_cost
FROM frames
""")
n, itok, otok, crd, cwr, cost = r["rows"][0]
print(f"{n} frames, ${cost or 0:.4f} total")
# Last 10 code cells executed in this project (any frame), with outcome.
# Add `WHERE e.frame_id = ?` with the current frame's id to scope to one
# frame.
host.query("""
SELECT e.frame_id, e.cell_index, e.language, e.kernel_kind, e.conda_env,
e.exit_status, substr(e.source, 1, 120) AS src,
json_array_length(e.files_written) AS n_files
FROM execution_log e
ORDER BY e.created_at DESC
LIMIT 10
""")
# How far into context is each root conversation in this project? Reads
# _messages length and compaction count without pulling the whole blob.
host.query("""
SELECT id, name,
json_array_length(context_data, '$._messages') AS n_messages,
json_extract(context_data, '$._compaction_count') AS compactions,
input_tokens, output_tokens
FROM frames
WHERE parent_frame_id IS NULL
ORDER BY updated_at DESC
""")
# Every artifact this project has, with current size/type, newest first.
host.query("""
SELECT a.filename, v.content_type, v.size_bytes, v.version_number,
a.is_user_upload, a.latest_version_id
FROM artifacts a
JOIN artifact_versions v ON a.latest_version_id = v.id
WHERE a.is_ephemeral = 0
ORDER BY v.created_at DESC
""")
SDK surface — which tool runs what
The host object is a Python SDK backed by host-side RPCs. Run help(host) / help(host.<x>) for signatures.
| Accessor |
Tool |
Returns |
host.query(sql, params, limit, df) |
repl |
Raw SQL over the tables above |
host.frames(...) |
repl |
List/search/detail frames (paginated messages) |
host.children() |
repl |
Live sub-agents (delegation-enabled profiles only) |
host.delegate(taskorlist, name=?, profile=?, output_schema=?, model=?) |
repl |
Spawn child agent(s), block until done (ultra-mode roots; requires [delegation] sdk_enabled). model= pins the child's model per request — e.g. a haiku-class id for cheap fan-outs. Blocks the cell — for long-running children run it in a background cell (a user message mid-call backgrounds it; a Stop / cell interrupt cancels the children) |
host.agents. / host.skills. |
repl |
Profile and skill CRUD — load customize skill |
host.submitoutput(output, completionbullets=[...]) |
repl |
Submit your structured result when your task carries an OUTPUT SCHEMA section (required before completing). Build the dict in-kernel — the payload rides the host-call wire, not your prose; on a validation/review bounce, mutate the dict in memory and resubmit (replaces the recorded output) |
host.compute.* |
repl |
Remote job submit/wait — load the compute skill it names |
host.artifacts(...) |
python |
Filtered artifact search (wraps the join above) |
host.artifactpath(vid) / host.artifactmarker(vid) |
python |
Resolve a version_id to a readable path / marker |
host.lineage[vid] |
python |
{code, messages, env, inputs} for one version |
host.llm(promptorlist, model=?, ...) |
python |
Single-turn completion via the host's API client. Omitting model= uses the Haiku-class kernel default (via [llm] kerneldefaultmodel); for harder reasoning pass model=host.current_model() — never hardcode a literal model id (they go stale) |
host.credentials.list() / .get(name) |
python |
User-configured credential metadata |
host.mcp(server, method, **kw) |
repl |
MCP/connector call — only exists in the repl tool; pass results to python/r via ./handoff/*.json |
The repl tool and the python tool are separate processes that share only the workspace directory — move data between them via ./handoff/*.json, not variables.