forked from chauncygu/collection-claude-code-source-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext.py
More file actions
225 lines (185 loc) · 8.02 KB
/
context.py
File metadata and controls
225 lines (185 loc) · 8.02 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"""Memory context building for system prompt injection.
Provides:
get_memory_context() — full context string for system prompt
find_relevant_memories() — keyword (+ optional AI) relevance filtering
truncate_index_content() — line + byte truncation with warning
"""
from __future__ import annotations
from pathlib import Path
from .store import (
USER_MEMORY_DIR,
INDEX_FILENAME,
MAX_INDEX_LINES,
MAX_INDEX_BYTES,
get_memory_dir,
get_index_content,
load_entries,
search_memory,
)
from .scan import scan_all_memories, format_memory_manifest, memory_freshness_text
from .types import MEMORY_SYSTEM_PROMPT
# ── Index truncation ───────────────────────────────────────────────────────
def truncate_index_content(raw: str) -> str:
"""Truncate MEMORY.md content to line AND byte limits, appending a warning.
Matches Claude Code's truncateEntrypointContent:
- Line-truncates first (natural boundary)
- Then byte-truncates at the last newline before the cap
- Appends which limit fired
"""
trimmed = raw.strip()
content_lines = trimmed.split("\n")
line_count = len(content_lines)
byte_count = len(trimmed.encode())
was_line_truncated = line_count > MAX_INDEX_LINES
was_byte_truncated = byte_count > MAX_INDEX_BYTES
if not was_line_truncated and not was_byte_truncated:
return trimmed
truncated = "\n".join(content_lines[:MAX_INDEX_LINES]) if was_line_truncated else trimmed
if len(truncated.encode()) > MAX_INDEX_BYTES:
# Cut at last newline before byte limit
raw_bytes = truncated.encode()
cut = raw_bytes[:MAX_INDEX_BYTES].rfind(b"\n")
truncated = raw_bytes[: cut if cut > 0 else MAX_INDEX_BYTES].decode(errors="replace")
if was_byte_truncated and not was_line_truncated:
reason = f"{byte_count:,} bytes (limit: {MAX_INDEX_BYTES:,}) — index entries are too long"
elif was_line_truncated and not was_byte_truncated:
reason = f"{line_count} lines (limit: {MAX_INDEX_LINES})"
else:
reason = f"{line_count} lines and {byte_count:,} bytes"
warning = (
f"\n\n> WARNING: {INDEX_FILENAME} is {reason}. "
"Only part of it was loaded. Keep index entries to one line under ~150 chars."
)
return truncated + warning
# ── System prompt context ──────────────────────────────────────────────────
def get_memory_context(include_guidance: bool = False) -> str:
"""Return memory context for injection into the system prompt.
Combines user-level and project-level MEMORY.md content (if present).
Returns empty string when no memories exist.
Args:
include_guidance: if True, prepend the full memory system guidance
(MEMORY_SYSTEM_PROMPT). Normally False since the
system prompt template already includes brief guidance.
"""
parts: list[str] = []
# User-level index
user_content = get_index_content("user")
if user_content:
truncated = truncate_index_content(user_content)
parts.append(truncated)
# Project-level index (labelled separately)
proj_content = get_index_content("project")
if proj_content:
truncated = truncate_index_content(proj_content)
parts.append(f"[Project memories]\n{truncated}")
if not parts:
return ""
body = "\n\n".join(parts)
if include_guidance:
return f"{MEMORY_SYSTEM_PROMPT}\n\n## MEMORY.md\n{body}"
return body
# ── Relevant memory finder ─────────────────────────────────────────────────
def find_relevant_memories(
query: str,
max_results: int = 5,
use_ai: bool = False,
config: dict | None = None,
) -> list[dict]:
"""Find memories relevant to a query.
Strategy:
1. Always: keyword match on name + description + content
2. If use_ai=True and config has a model: use a small AI call to rank
Returns:
List of dicts with keys: name, description, type, scope, content,
file_path, mtime_s, freshness_text
"""
# Step 1: Keyword filter
keyword_results = search_memory(query)
if not keyword_results:
return []
if not use_ai or not config:
# Return top max_results by recency (newest first)
from .scan import scan_all_memories
headers = scan_all_memories()
path_to_mtime = {h.file_path: h.mtime_s for h in headers}
results = []
for entry in keyword_results[:max_results * 3]:
mtime_s = path_to_mtime.get(entry.file_path, 0)
results.append({
"name": entry.name,
"description": entry.description,
"type": entry.type,
"scope": entry.scope,
"content": entry.content,
"file_path": entry.file_path,
"mtime_s": mtime_s,
"freshness_text": memory_freshness_text(mtime_s),
"confidence": entry.confidence,
"source": entry.source,
})
results.sort(key=lambda r: r["mtime_s"], reverse=True)
return results[:max_results]
# Step 2: AI-powered relevance selection (optional, lightweight)
return _ai_select_memories(query, keyword_results, max_results, config)
def _ai_select_memories(
query: str,
candidates: list,
max_results: int,
config: dict,
) -> list[dict]:
"""Use a fast AI call to select the most relevant memories from candidates.
Falls back to keyword results on any error.
"""
try:
from providers import stream, AssistantTurn
from .scan import scan_all_memories
headers = scan_all_memories()
path_to_mtime = {h.file_path: h.mtime_s for h in headers}
# Build manifest of candidates only
manifest_lines = []
for i, e in enumerate(candidates):
manifest_lines.append(f"{i}: [{e.type}] {e.name} — {e.description}")
manifest = "\n".join(manifest_lines)
system = (
"You select memories relevant to a query. "
"Return a JSON object with key 'indices' containing a list of integer indices "
f"(0-based) from the provided list. Select at most {max_results} entries. "
"Only include indices clearly relevant to the query. Return {\"indices\": []} if none."
)
messages = [{"role": "user", "content": f"Query: {query}\n\nMemories:\n{manifest}"}]
result_text = ""
for event in stream(
model=config.get("model", "claude-haiku-4-5-20251001"),
system=system,
messages=messages,
tool_schemas=[],
config={**config, "max_tokens": 256, "no_tools": True},
):
if isinstance(event, AssistantTurn):
result_text = event.text
break
import json as _json
parsed = _json.loads(result_text)
selected_indices = [int(i) for i in parsed.get("indices", []) if isinstance(i, int)]
except Exception:
# Fall back to keyword results
selected_indices = list(range(min(max_results, len(candidates))))
results = []
for i in selected_indices[:max_results]:
if i < 0 or i >= len(candidates):
continue
entry = candidates[i]
mtime_s = path_to_mtime.get(entry.file_path, 0) if "path_to_mtime" in dir() else 0
results.append({
"name": entry.name,
"description": entry.description,
"type": entry.type,
"scope": entry.scope,
"content": entry.content,
"file_path": entry.file_path,
"mtime_s": mtime_s,
"freshness_text": memory_freshness_text(mtime_s),
"confidence": entry.confidence,
"source": entry.source,
})
return results