-
Notifications
You must be signed in to change notification settings - Fork 53.5k
Expand file tree
/
Copy pathhermes_state_timeline.py
More file actions
167 lines (146 loc) · 7.83 KB
/
Copy pathhermes_state_timeline.py
File metadata and controls
167 lines (146 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""Read-only prompt index and bounded transcript jumps; no transcript-wide payload hydration."""
from __future__ import annotations
import re
from contextlib import contextmanager
from agent.compaction_display import project_compaction_message_for_display
from agent.context_compressor import user_originated_turn_view
from hermes_state_messages import DISPLAY_VISIBLE_SQL
_SYNTHETIC_PROMPT = re.compile(
r"^\s*(?:\[IMPORTANT: Background process |\[ASYNC (?:DELEGATION )?(?:BATCH )?COMPLETE\b|"
r"A background fan-out of \d+ subagent\(s\) you dispatched earlier has finished\.|"
r"A background subagent you dispatched earlier has finished\.)",
re.IGNORECASE,
)
def _prompt_preview(db, content, display_kind, summary):
message = project_compaction_message_for_display({
"role": "user", "content": db._decode_content(content),
"display_kind": display_kind, "_compressed_summary": bool(summary),
})
if message is None or user_originated_turn_view(message) is None:
return ""
content = message.get("content")
if isinstance(content, list):
content = " ".join(
part if isinstance(part, str) else part.get("text", "")
for part in content if isinstance(part, (str, dict)))
if not isinstance(content, str):
return ""
text = " ".join(content.split())
if not text or _SYNTHETIC_PROMPT.match(text):
return ""
return text if len(text) <= 120 else text[:119].rstrip() + "…"
@contextmanager
def _snapshot(db):
# The count and page must see the same compaction/rewind generation.
with db._read_ctx() as conn:
conn.execute("BEGIN")
try:
yield conn
finally:
if conn.in_transaction:
conn.execute("ROLLBACK")
def _display_rows_sql(conn, session_id, *, users_only=False):
"""Return only representative ids and their stable first-row order, never bodies.
Legacy stores cannot backfill on a GET. SQL groups their payload identities in
SQLite; only user content crosses the Python boundary for carrier normalization.
Current stores use the durable display index, including protected-tail copies.
"""
filters = (" AND role = 'user'" if users_only else "") + DISPLAY_VISIBLE_SQL
indexed = conn.execute(
"SELECT 1 FROM messages WHERE session_id = ? AND (active = 1 OR compacted = 1) "
f"{filters} AND (display_order IS NULL OR display_identity IS NULL) LIMIT 1",
(session_id,),
).fetchone() is None
if indexed:
return f"""WITH display_rows AS (
SELECT (SELECT candidate.id FROM messages candidate
WHERE candidate.session_id = :sid
AND candidate.display_order = m.display_order
AND (candidate.active = 1 OR candidate.compacted = 1){DISPLAY_VISIBLE_SQL}
ORDER BY candidate.active DESC, candidate.id DESC LIMIT 1) AS row_id,
m.display_order AS sort_id
FROM messages m WHERE session_id = :sid AND (active = 1 OR compacted = 1){filters}
GROUP BY m.display_order
)"""
return f"""WITH ranked AS (
SELECT id, MIN(id) OVER identity AS sort_id,
ROW_NUMBER() OVER (identity ORDER BY active DESC, id DESC) AS preference
FROM messages WHERE session_id = :sid AND (active = 1 OR compacted = 1){filters}
WINDOW identity AS (PARTITION BY role,
CASE WHEN role = 'user' THEN timeline_identity_content(content, display_kind) ELSE content END,
timestamp, tool_call_id, tool_calls, tool_name)
), display_rows AS (SELECT id AS row_id, sort_id FROM ranked WHERE preference = 1)"""
def _register_functions(db, conn):
from agent.context_compressor import split_user_originated_turn
def identity_content(content, display_kind):
handoff, live = split_user_originated_turn({
"role": "user", "content": db._decode_content(content), "display_kind": display_kind})
return db._encode_content(live.get("content")) if handoff is not None and live is not None else content
conn.create_function("timeline_identity_content", 2, identity_content, deterministic=True)
conn.create_function("timeline_preview", 3,
lambda content, kind, summary: _prompt_preview(db, content, kind, summary),
deterministic=True)
def get_session_messages_around(db, session_id, row_id, *, limit=120):
"""Read at most *limit* display rows starting at an exact human prompt.
Existence probes/counts contain ids only. Full payloads are fetched only for
the selected bounded page, even when the anchor is deep in a transcript.
"""
with _snapshot(db) as conn:
_register_functions(db, conn)
anchor = conn.execute(
"SELECT content, display_kind, _compressed_summary FROM messages "
"WHERE session_id = ? AND id = ? AND role = 'user' AND (active = 1 OR compacted = 1)",
(session_id, row_id),
).fetchone()
if anchor is None or not _prompt_preview(db, *anchor):
return None
sql = _display_rows_sql(conn, session_id)
params = {"sid": session_id, "row_id": row_id, "limit": limit}
selected = conn.execute(sql + """
SELECT sort_id FROM display_rows WHERE row_id = :row_id
""", params).fetchone()
if selected is None:
return None
params["start"] = selected["sort_id"]
counts = conn.execute(sql + """
SELECT COUNT(*) AS total, COALESCE(SUM(sort_id < :start), 0) AS offset FROM display_rows
""", params).fetchone()
rows = conn.execute(sql + """
SELECT m.* FROM (SELECT row_id, sort_id FROM display_rows
WHERE sort_id >= :start ORDER BY sort_id LIMIT :limit) AS page
JOIN messages m ON m.id = page.row_id ORDER BY page.sort_id
""", params).fetchall()
messages = [db._row_to_message_dict(row, warn_context="timeline jump", summary_flag=True) for row in rows]
return {"messages": messages, "pagination": {
"row_id": row_id, "limit": limit, "returned": len(messages), "order": "oldest",
"offset": counts["offset"], "total": counts["total"],
"has_older": counts["offset"] > 0,
"has_newer": counts["offset"] + len(messages) < counts["total"],
}}
def get_session_timeline(db, session_id, *, limit=500, after_row_id=0):
"""Chronological prompts. Cursor is the first physical row id of a logical turn."""
with _snapshot(db) as conn:
_register_functions(db, conn)
sql = _display_rows_sql(conn, session_id, users_only=True) + """,
prompts AS MATERIALIZED (
SELECT row_id, sort_id, m.timestamp,
timeline_preview(m.content, m.display_kind, m._compressed_summary) AS preview
FROM display_rows JOIN messages m ON m.id = row_id
), eligible AS MATERIALIZED (SELECT * FROM prompts WHERE preview <> '')
"""
params = {"sid": session_id, "after": after_row_id, "limit": limit + 1}
rows = conn.execute(sql + """
SELECT row_id, sort_id, timestamp, preview, (SELECT COUNT(*) FROM eligible) AS total
FROM eligible WHERE sort_id > :after ORDER BY sort_id LIMIT :limit
""", params).fetchall()
total = rows[0]["total"] if rows else conn.execute(
sql + "SELECT COUNT(*) FROM eligible", params).fetchone()[0]
has_more = len(rows) > limit
page = rows[:limit]
return {
"entries": [{"row_id": row["row_id"], "preview": row["preview"], "timestamp": row["timestamp"]}
for row in page],
"pagination": {"limit": limit, "after_row_id": after_row_id, "returned": len(page),
"total": total, "has_more": has_more,
"next_cursor": page[-1]["sort_id"] if has_more else None},
}