Cookbook / Agent memory · Intermediate
Agent trajectory timeline
An agent runtime appends one step per tool call to a session's timeline. On every turn it asks for the last few steps, the page before those, and everything since a cutoff; a supervisor asks for the newest steps across all sessions. Four constraints make those newest-first reads correct by construction, and one statement migrates a timeline that was written the other way.
The graph is
(:Session {id})-[:STEP {ts, seq, tokens}]->(:Step {id, kind, text, at})-[:CALLED]->(:Tool {name}).
ts is the sort key: epoch milliseconds, stored as an integer. at keeps the
same instant as a datetime for display. tokens sits on the relationship, so
a budget read never touches the Step nodes. The catalog of problems relate
solves lists this pattern as the
agent trajectory timeline.
Lock the timeline down
Four constraints. session_id makes the per-session anchor one node, and
tool_name lets MERGE keep one node per tool, so a read that hops on to the
tool cannot fan out. The last two say that every STEP has a ts, and that it
is an integer.
CREATE CONSTRAINT session_id IF NOT EXISTS FOR (s:Session) REQUIRE s.id IS UNIQUE;
CREATE CONSTRAINT tool_name IF NOT EXISTS FOR (t:Tool) REQUIRE t.name IS UNIQUE;
CREATE CONSTRAINT step_ts IF NOT EXISTS FOR ()-[r:STEP]-() REQUIRE r.ts IS :: INTEGER;
CREATE CONSTRAINT step_ts_nn IF NOT EXISTS FOR ()-[r:STEP]-() REQUIRE r.ts IS NOT NULL;
SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties
RETURN name, type, entityType, labelsOrTypes, properties
ORDER BY name| name | type | entityType | labelsOrTypes | properties |
|---|---|---|---|---|
| "session_id" | "NODE_PROPERTY_UNIQUENESS" | "NODE" | ["Session"] | ["id"] |
| "step_ts" | "RELATIONSHIP_PROPERTY_TYPE" | "RELATIONSHIP" | ["STEP"] | ["ts"] |
| "step_ts_nn" | "RELATIONSHIP_PROPERTY_EXISTENCE" | "RELATIONSHIP" | ["STEP"] | ["ts"] |
| "tool_name" | "NODE_PROPERTY_UNIQUENESS" | "NODE" | ["Tool"] | ["name"] |
4 rows · constraints added: 4
| name | type | entityType | labelsOrTypes | properties | |--------------|-----------------------------------|----------------|---------------|------------| | "session_id" | "NODE_PROPERTY_UNIQUENESS" | "NODE" | ["Session"] | ["id"] | | "step_ts" | "RELATIONSHIP_PROPERTY_TYPE" | "RELATIONSHIP" | ["STEP"] | ["ts"] | | "step_ts_nn" | "RELATIONSHIP_PROPERTY_EXISTENCE" | "RELATIONSHIP" | ["STEP"] | ["ts"] | | "tool_name" | "NODE_PROPERTY_UNIQUENESS" | "NODE" | ["Tool"] | ["name"] | 4 rows · constraints added: 4
Why an integer? Relate keeps each node's relationships of one type in ts
order when ts is an integer
(ADR-0035). A read with one anchor node,
one typed hop, and ORDER BY t.ts DESC LIMIT n then walks the newest
relationships first and stops after n, instead of sorting the whole session.
Note Only an integer
tsis kept in order. Over atsthat holds a datetime or an ISO string, Relate currently answers the same read with the first steps ever written, labelled newest-first, and no error. The twotsconstraints make that shape impossible to write.
The type and the presence are two constraints because a property type
constraint cannot carry NOT NULL:
CREATE CONSTRAINT step_ts_strict IF NOT EXISTS
FOR ()-[r:STEP]-() REQUIRE r.ts IS :: INTEGER NOT NULLerror: syntax error: INTEGER NOT NULL is not supported in property type constraints.
Load three sessions
The fixture is one steps list: 40 steps in each of three sessions, plus one
more for s1. That last row is s1's step 0, backfilled after a restart: it
is written last but carries the oldest timestamp, so creation order and time
order disagree. datetime(r.at).epochMillis turns each ISO timestamp into the
integer sort key. A step's id is its session and sequence number.
Show the setup step
UNWIND $steps AS r
MERGE (s:Session {id: r.sid})
MERGE (tool:Tool {name: r.tool})
CREATE (s)-[:STEP {ts: datetime(r.at).epochMillis, seq: r.seq, tokens: r.tokens}]->
(:Step {id: r.sid + ':' + toString(r.seq), kind: r.kind, text: r.text,
at: datetime(r.at)})-[:CALLED]->(tool);
MATCH (s:Session) WITH count(s) AS sessions
MATCH (t:Tool) WITH sessions, count(t) AS tools
MATCH ()-[t:STEP]->()
RETURN sessions, tools, count(t) AS steps| sessions | tools | steps |
|---|---|---|
| 3 | 6 | 121 |
1 row · nodes created: 130, relationships created: 242, properties set: 856, labels added: 130
| sessions | tools | steps | |----------|-------|-------| | 3 | 6 | 121 | 1 row · nodes created: 130, relationships created: 242, properties set: 856, labels added: 130
Six distinct tool names in the fixture, six Tool nodes.
A bad append is refused
A runtime that copies a timestamp as a datetime, instead of converting it, fails loudly instead of corrupting the order:
MATCH (s:Session {id: 's1'}), (tool:Tool {name: 'shell'})
CREATE (s)-[:STEP {ts: datetime('2024-01-07T00:42:00Z'), seq: 41, tokens: 10}]->
(:Step {id: 's1:41', kind: 'answer', text: 'copied a datetime',
at: datetime('2024-01-07T00:42:00Z')})-[:CALLED]->(tool)error: semantic error: ConstraintValidationFailed: `step_ts` property type
A step with no ts at all fails the same way, on step_ts_nn.
The last N steps
The agent's per-turn read: one session, newest first.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
RETURN t.seq AS seq, x.kind AS kind, x.text AS text
ORDER BY t.ts DESC
LIMIT $nParameters
| seq | kind | text |
|---|---|---|
| 40 | "answer" | "answer: run the epochMillis probe" |
| 39 | "observation" | "observation: ask about epochMillis" |
2 rows
| seq | kind | text | |-----|---------------|--------------------------------------| | 40 | "answer" | "answer: run the epochMillis probe" | | 39 | "observation" | "observation: ask about epochMillis" | 2 rows
The next page
"Show me more" is the same read with SKIP. It stays on the ordered walk: the
walk stops after skip + n steps.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
RETURN t.seq AS seq, x.kind AS kind
ORDER BY t.ts DESC
SKIP $skip
LIMIT $nParameters
| seq | kind |
|---|---|
| 38 | "tool_call" |
| 37 | "plan" |
2 rows
| seq | kind | |-----|-------------| | 38 | "tool_call" | | 37 | "plan" | 2 rows
Everything since a cutoff
The caller sends an ISO timestamp. LET names the cutoff once, as epoch
milliseconds, and FILTER keeps the steps after it (both are Cypher 25; a
WHERE says the same). With no LIMIT, the read sorts the steps it keeps
instead of walking in order, which is fine for a bounded window.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
LET cutoff = datetime($since).epochMillis
FILTER t.ts > cutoff
RETURN t.seq AS seq, x.kind AS kind
ORDER BY t.ts DESCParameters
| seq | kind |
|---|---|
| 40 | "answer" |
| 39 | "observation" |
| 38 | "tool_call" |
| 37 | "plan" |
4 rows
| seq | kind | |-----|---------------| | 40 | "answer" | | 39 | "observation" | | 38 | "tool_call" | | 37 | "plan" | 4 rows
The oldest step
Ascending order finds the backfilled step, even though it was written last.
datetime.fromEpochMillis turns the integer back into a datetime inside
Cypher; it equals the at stored on the step, so a runtime that keeps only
ts loses nothing.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
WITH t, x ORDER BY t.ts ASC LIMIT 1
RETURN t.seq AS seq, t.ts AS ts, x.at AS at,
datetime.fromEpochMillis(t.ts) = x.at AS matches_at, x.text AS textParameters
| seq | ts | at | matches_at | text |
|---|---|---|---|---|
| 0 | 1704585600000 | "2024-01-07T00:00:00Z" | true | "system: load task context (backfilled after a restart)" |
1 row
| seq | ts | at | matches_at | text | |-----|---------------|------------------------|------------|----------------------------------------------------------| | 0 | 1704585600000 | "2024-01-07T00:00:00Z" | true | "system: load task context (backfilled after a restart)" | 1 row
Note Use
datetime.fromEpochMillis(ms). Relate currently returns0000-01-01T00:00Zfordatetime({epochMillis: ms})anddatetime({epochSeconds: s})instead of the instant.
The session on a time axis
Every step of one session, oldest first, with the datetime at beside its
kind. The backfilled step 0 opens the axis, although it was written last.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
RETURN t.seq AS seq, x.kind AS kind, x.at AS at
ORDER BY t.tsParameters
| seq | kind | at |
|---|---|---|
| 0 | "system" | "2024-01-07T00:00:00Z" |
| 1 | "plan" | "2024-01-07T00:01:14.647Z" |
| 2 | "tool_call" | "2024-01-07T00:02:32.928Z" |
| 3 | "observation" | "2024-01-07T00:03:51.476Z" |
| 4 | "plan" | "2024-01-07T00:04:48.677Z" |
| 5 | "tool_call" | "2024-01-07T00:05:42.969Z" |
| 6 | "observation" | "2024-01-07T00:06:30.713Z" |
| 7 | "plan" | "2024-01-07T00:07:26.034Z" |
| 8 | "tool_call" | "2024-01-07T00:08:45.661Z" |
| 9 | "observation" | "2024-01-07T00:09:43.126Z" |
| 10 | "plan" | "2024-01-07T00:10:58.53Z" |
| 11 | "tool_call" | "2024-01-07T00:11:56.33Z" |
| 12 | "observation" | "2024-01-07T00:13:00.607Z" |
| 13 | "plan" | "2024-01-07T00:13:51.176Z" |
| 14 | "tool_call" | "2024-01-07T00:15:02.834Z" |
| 15 | "observation" | "2024-01-07T00:16:04.479Z" |
| 16 | "plan" | "2024-01-07T00:17:08.421Z" |
| 17 | "tool_call" | "2024-01-07T00:18:00.494Z" |
| 18 | "observation" | "2024-01-07T00:19:10.824Z" |
| 19 | "plan" | "2024-01-07T00:19:55.859Z" |
| 20 | "tool_call" | "2024-01-07T00:21:11.66Z" |
| 21 | "observation" | "2024-01-07T00:22:24.171Z" |
| 22 | "plan" | "2024-01-07T00:23:22.179Z" |
| 23 | "tool_call" | "2024-01-07T00:24:12.89Z" |
| 24 | "observation" | "2024-01-07T00:25:24.763Z" |
| 25 | "plan" | "2024-01-07T00:26:16.385Z" |
| 26 | "tool_call" | "2024-01-07T00:27:33.289Z" |
| 27 | "observation" | "2024-01-07T00:28:30.635Z" |
| 28 | "plan" | "2024-01-07T00:29:24.218Z" |
| 29 | "tool_call" | "2024-01-07T00:30:16.853Z" |
| 30 | "observation" | "2024-01-07T00:31:01.883Z" |
| 31 | "plan" | "2024-01-07T00:31:48.169Z" |
| 32 | "tool_call" | "2024-01-07T00:32:39.744Z" |
| 33 | "observation" | "2024-01-07T00:33:53.68Z" |
| 34 | "plan" | "2024-01-07T00:35:00.238Z" |
| 35 | "tool_call" | "2024-01-07T00:35:50.106Z" |
| 36 | "observation" | "2024-01-07T00:36:51.032Z" |
| 37 | "plan" | "2024-01-07T00:38:00.391Z" |
| 38 | "tool_call" | "2024-01-07T00:39:17.092Z" |
| 39 | "observation" | "2024-01-07T00:40:14.073Z" |
| 40 | "answer" | "2024-01-07T00:41:14.04Z" |
41 rows
| seq | kind | at | |-----|---------------|----------------------------| | 0 | "system" | "2024-01-07T00:00:00Z" | | 1 | "plan" | "2024-01-07T00:01:14.647Z" | | 2 | "tool_call" | "2024-01-07T00:02:32.928Z" | | 3 | "observation" | "2024-01-07T00:03:51.476Z" | | 4 | "plan" | "2024-01-07T00:04:48.677Z" | | 5 | "tool_call" | "2024-01-07T00:05:42.969Z" | | 6 | "observation" | "2024-01-07T00:06:30.713Z" | | 7 | "plan" | "2024-01-07T00:07:26.034Z" | | 8 | "tool_call" | "2024-01-07T00:08:45.661Z" | | 9 | "observation" | "2024-01-07T00:09:43.126Z" | | 10 | "plan" | "2024-01-07T00:10:58.53Z" | | 11 | "tool_call" | "2024-01-07T00:11:56.33Z" | | 12 | "observation" | "2024-01-07T00:13:00.607Z" | | 13 | "plan" | "2024-01-07T00:13:51.176Z" | | 14 | "tool_call" | "2024-01-07T00:15:02.834Z" | | 15 | "observation" | "2024-01-07T00:16:04.479Z" | | 16 | "plan" | "2024-01-07T00:17:08.421Z" | | 17 | "tool_call" | "2024-01-07T00:18:00.494Z" | | 18 | "observation" | "2024-01-07T00:19:10.824Z" | | 19 | "plan" | "2024-01-07T00:19:55.859Z" | | 20 | "tool_call" | "2024-01-07T00:21:11.66Z" | | 21 | "observation" | "2024-01-07T00:22:24.171Z" | | 22 | "plan" | "2024-01-07T00:23:22.179Z" | | 23 | "tool_call" | "2024-01-07T00:24:12.89Z" | | 24 | "observation" | "2024-01-07T00:25:24.763Z" | | 25 | "plan" | "2024-01-07T00:26:16.385Z" | | 26 | "tool_call" | "2024-01-07T00:27:33.289Z" | | 27 | "observation" | "2024-01-07T00:28:30.635Z" | | 28 | "plan" | "2024-01-07T00:29:24.218Z" | | 29 | "tool_call" | "2024-01-07T00:30:16.853Z" | | 30 | "observation" | "2024-01-07T00:31:01.883Z" | | 31 | "plan" | "2024-01-07T00:31:48.169Z" | | 32 | "tool_call" | "2024-01-07T00:32:39.744Z" | | 33 | "observation" | "2024-01-07T00:33:53.68Z" | | 34 | "plan" | "2024-01-07T00:35:00.238Z" | | 35 | "tool_call" | "2024-01-07T00:35:50.106Z" | | 36 | "observation" | "2024-01-07T00:36:51.032Z" | | 37 | "plan" | "2024-01-07T00:38:00.391Z" | | 38 | "tool_call" | "2024-01-07T00:39:17.092Z" | | 39 | "observation" | "2024-01-07T00:40:14.073Z" | | 40 | "answer" | "2024-01-07T00:41:14.04Z" | 41 rows
The timeline draws those rows, one lane per kind. Its slider sets the
since cutoff from Everything since a cutoff
and runs that step again for the same session, so the steps it keeps light
up.
The session step's recorded rows on a time axis: 41 rows in 5 lanes by kind, from 2024-01-07T00:00:00Z to 2024-01-07T00:41:14.04Z.
| kind | rows | seq | earliest at | latest at |
|---|---|---|---|---|
| system | 1 | 0 | 2024-01-07T00:00:00Z | 2024-01-07T00:00:00Z |
| plan | 13 | 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37 | 2024-01-07T00:01:14.647Z | 2024-01-07T00:38:00.391Z |
| tool_call | 13 | 2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38 | 2024-01-07T00:02:32.928Z | 2024-01-07T00:39:17.092Z |
| observation | 13 | 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39 | 2024-01-07T00:03:51.476Z | 2024-01-07T00:40:14.073Z |
| answer | 1 | 40 | 2024-01-07T00:41:14.04Z | 2024-01-07T00:41:14.04Z |
Live: the drawing reads its rows again from each run of the statement it draws on the page's database in your browser, so an edit to the statement or its parameters redraws it; Tab reaches the marks and the arrow keys move along a lane and between lanes, each mark read out with its values; where the page names an as-of statement, a time slider runs that statement again with the time as its parameter and lights up the rows it returns.
Which tool each step called
One more hop reaches the tool. Tool names are unique, so each step reaches
exactly one Tool and the read returns exactly $n rows.
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)-[:CALLED]->(tool:Tool)
RETURN t.seq AS seq, x.kind AS kind, tool.name AS tool
ORDER BY t.ts DESC
LIMIT $nParameters
| seq | kind | tool |
|---|---|---|
| 40 | "answer" | "shell" |
| 39 | "observation" | "ask_user" |
| 38 | "tool_call" | "shell" |
3 rows
| seq | kind | tool | |-----|---------------|------------| | 40 | "answer" | "shell" | | 39 | "observation" | "ask_user" | | 38 | "tool_call" | "shell" | 3 rows
To see the shape, return the paths. Open the graph view:
MATCH p = (s:Session {id: $sid})-[t:STEP]->(x:Step)-[:CALLED]->(tool:Tool)
WITH p, t ORDER BY t.ts DESC LIMIT $n
RETURN pParameters
// 3 rows
(n0:Session {id: "s1"})-[r78:STEP {seq: 40, tokens: 782, ts: 1704588074040}]->(n48:Step {at: "2024-01-07T00:41:14.04Z", id: "s1:40", kind: "answer", text: "answer: run the epochMillis probe"})-[r79:CALLED]->(n3:Tool {name: "shell"})
(n0)-[r76:STEP {seq: 39, tokens: 358, ts: 1704588014073}]->(n47:Step {at: "2024-01-07T00:40:14.073Z", id: "s1:39", kind: "observation", text: "observation: ask about epochMillis"})-[r77:CALLED]->(n8:Tool {name: "ask_user"})
(n0)-[r74:STEP {seq: 38, tokens: 435, ts: 1704587957092}]->(n46:Step {at: "2024-01-07T00:39:17.092Z", id: "s1:38", kind: "tool_call", text: "tool_call: run the epochMillis probe"})-[r75:CALLED]->(n3)Token budget per session
count and sum over each session's STEP relationships. The tokens are on
the relationship, so the read never loads a Step node.
MATCH (s:Session)-[t:STEP]->()
RETURN s.id AS session, count(t) AS steps, sum(t.tokens) AS tokens
ORDER BY session| session | steps | tokens |
|---|---|---|
| "s1" | 41 | 18485 |
| "s2" | 40 | 18201 |
| "s3" | 40 | 19583 |
3 rows
| session | steps | tokens | |---------|-------|--------| | "s1" | 41 | 18485 | | "s2" | 40 | 18201 | | "s3" | 40 | 19583 | 3 rows
The newest steps across all sessions
A supervisor wants the newest steps overall. The anchor now matches every
session. Session s3 ran last, so it owns the newest three.
MATCH (s:Session)-[t:STEP]->(x:Step)
RETURN s.id AS session, t.seq AS seq, x.kind AS kind
ORDER BY t.ts DESC, t.seq ASC
LIMIT $nParameters
| session | seq | kind |
|---|---|---|
| "s3" | 40 | "answer" |
| "s3" | 39 | "observation" |
| "s3" | 38 | "tool_call" |
3 rows
| session | seq | kind | |---------|-----|---------------| | "s3" | 40 | "answer" | | "s3" | 39 | "observation" | | "s3" | 38 | "tool_call" | 3 rows
Note Keep the second sort key (
t.seq ASC) when the anchor matches more than one node. It also breaks ties between equal timestamps. Without it, Relate currently returns the newest steps of the first session it walks (heres1) instead of the newest overall.
Append a step
A runtime appends the way the fixture was loaded: find the session, MERGE
the tool, create the step. A live runtime passes datetime().epochMillis for
now; this step uses a fixed timestamp so its result does not change. The same
step then reads the last three, and the new step comes first.
MATCH (s:Session {id: $sid})
MERGE (tool:Tool {name: $tool})
CREATE (s)-[:STEP {ts: datetime($at).epochMillis, seq: $seq, tokens: $tokens}]->
(:Step {id: $sid + ':' + toString($seq), kind: $kind, text: $text,
at: datetime($at)})-[:CALLED]->(tool);
MATCH (s:Session {id: $sid})-[t:STEP]->(x:Step)
RETURN t.seq AS seq, x.kind AS kind, x.text AS text
ORDER BY t.ts DESC
LIMIT $nParameters
| seq | kind | text |
|---|---|---|
| 41 | "answer" | "answer: the latest-N read is correct" |
| 40 | "answer" | "answer: run the epochMillis probe" |
| 39 | "observation" | "observation: ask about epochMillis" |
3 rows · nodes created: 1, relationships created: 2, properties set: 7, labels added: 1
| seq | kind | text | |-----|---------------|----------------------------------------| | 41 | "answer" | "answer: the latest-N read is correct" | | 40 | "answer" | "answer: run the epochMillis probe" | | 39 | "observation" | "observation: ask about epochMillis" | 3 rows · nodes created: 1, relationships created: 2, properties set: 7, labels added: 1
Migrate a timeline written with datetimes
A timeline written before these constraints existed often has ts as a
datetime. To stand in for one, this step drops the two ts constraints and
writes session s2 again as session legacy, with ts: datetime(r.at):
DROP CONSTRAINT step_ts;
DROP CONSTRAINT step_ts_nn;
UNWIND $steps AS r
WITH r WHERE r.sid = 's2'
MERGE (s:Session {id: 'legacy'})
MERGE (tool:Tool {name: r.tool})
CREATE (s)-[:STEP {ts: datetime(r.at), seq: r.seq, tokens: r.tokens}]->
(:Step {id: 'legacy:' + toString(r.seq), kind: r.kind, text: r.text,
at: datetime(r.at)})-[:CALLED]->(tool);
MATCH ()-[t:STEP]->()
RETURN valueType(t.ts) AS ts_type, count(t) AS steps
ORDER BY ts_type| ts_type | steps |
|---|---|
| "INTEGER NOT NULL" | 122 |
| "ZONED DATETIME NOT NULL" | 40 |
2 rows · nodes created: 41, relationships created: 80, properties set: 281, labels added: 41, constraints removed: 2
| ts_type | steps | |---------------------------|-------| | "INTEGER NOT NULL" | 122 | | "ZONED DATETIME NOT NULL" | 40 | 2 rows · nodes created: 41, relationships created: 80, properties set: 281, labels added: 41, constraints removed: 2
Order matters. step_ts checks the relationships that already exist, so it
cannot come back before the data is fixed:
CREATE CONSTRAINT step_ts IF NOT EXISTS FOR ()-[r:STEP]-() REQUIRE r.ts IS :: INTEGERerror: semantic error: ConstraintValidationFailed: `step_ts` property type
The migration rewrites every datetime ts as epoch milliseconds. Only
datetimes match the WHERE, so a second run migrates nothing. Each SET
also moves the relationship to its place in the ts order.
MATCH ()-[t:STEP]->()
WHERE valueType(t.ts) STARTS WITH 'ZONED DATETIME'
SET t.ts = t.ts.epochMillis
RETURN count(t) AS migrated| migrated |
|---|
| 40 |
1 row · properties set: 40
| migrated | |----------| | 40 | 1 row · properties set: 40
Then lock it in again, and the latest-N read on the migrated session is correct:
CREATE CONSTRAINT step_ts IF NOT EXISTS FOR ()-[r:STEP]-() REQUIRE r.ts IS :: INTEGER;
CREATE CONSTRAINT step_ts_nn IF NOT EXISTS FOR ()-[r:STEP]-() REQUIRE r.ts IS NOT NULL;
MATCH (s:Session {id: 'legacy'})-[t:STEP]->(x:Step)
RETURN t.seq AS seq, x.kind AS kind
ORDER BY t.ts DESC
LIMIT 3| seq | kind |
|---|---|
| 40 | "answer" |
| 39 | "observation" |
| 38 | "tool_call" |
3 rows · constraints added: 2
| seq | kind | |-----|---------------| | 40 | "answer" | | 39 | "observation" | | 38 | "tool_call" | 3 rows · constraints added: 2
The companion example runs the same reads from the relate CLI against a
file, one process per read, and times the ordered walk against a full sort.
About the data
- Dataset
- 121 steps an agent logged in three sessions, each with a kind, the tool it called, a line of text, a timestamp, and a token count (20 KB)
- Source
- Synthetic, generated with a fixed seed by the examples project's 11-trajectory-timeline/generate.py
- License
- Apache-2.0
Every data block is published beside this page as data/<name>.json, and the whole recipe as markdown.
More in Agent memory
- Bi-temporal decisionsA decision log with two time axes answers both "what was true on this date?" and "what did the agent believe on this date?", through late corrections, a rejection, and a planned change.
- Long-term memory for an agentOne session writes facts and decisions from a chat into a graph; a later session recalls them, with newer decisions superseding older ones and the history kept.
- Tool-call provenance for an agentRecord every memory tool call an agent makes in the graph it touched, then audit which call wrote a fact, who read it since, and what the failed calls left behind.