bone-agent 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/config.yaml.example +8 -0
- package/package.json +3 -2
- package/prompts/main/ask_questions.md +31 -0
- package/prompts/main/batch_independent_calls.md +5 -0
- package/prompts/main/casual_interactions.md +11 -0
- package/prompts/main/code_references.md +8 -0
- package/prompts/main/communication_style.md +12 -0
- package/prompts/main/context_reliability.md +12 -0
- package/prompts/main/conversational_tool_calling.md +15 -0
- package/prompts/main/dream.md +36 -0
- package/prompts/main/editing_pattern.md +13 -0
- package/prompts/main/error_handling.md +6 -0
- package/prompts/main/exploration_pattern.md +21 -0
- package/prompts/main/intro.md +1 -0
- package/prompts/main/obsidian.md +16 -0
- package/prompts/main/obsidian_project.md +79 -0
- package/prompts/main/professional_objectivity.md +3 -0
- package/prompts/main/targeted_searching.md +10 -0
- package/prompts/main/task_lists_pattern.md +8 -0
- package/prompts/main/temp_folder.md +9 -0
- package/prompts/main/think_before_acting.md +10 -0
- package/prompts/main/tone_and_style.md +4 -0
- package/prompts/main/tool_preferences.md +24 -0
- package/prompts/main/trust_subagent_context.md +21 -0
- package/prompts/main/when_to_use_sub_agent.md +7 -0
- package/prompts/micro/ask_questions.md +1 -0
- package/prompts/micro/batch_independent_calls.md +1 -0
- package/prompts/micro/casual_interactions.md +1 -0
- package/prompts/micro/code_references.md +1 -0
- package/prompts/micro/communication_style.md +1 -0
- package/prompts/micro/context_reliability.md +1 -0
- package/prompts/micro/conversational_tool_calling.md +1 -0
- package/prompts/micro/editing_pattern.md +1 -0
- package/prompts/micro/error_handling.md +1 -0
- package/prompts/micro/exploration_pattern.md +1 -0
- package/prompts/micro/intro.md +1 -0
- package/prompts/micro/obsidian.md +4 -0
- package/prompts/micro/obsidian_project.md +5 -0
- package/prompts/micro/professional_objectivity.md +1 -0
- package/prompts/micro/targeted_searching.md +1 -0
- package/prompts/micro/task_lists_pattern.md +1 -0
- package/prompts/micro/temp_folder.md +1 -0
- package/prompts/micro/think_before_acting.md +5 -0
- package/prompts/micro/tone_and_style.md +1 -0
- package/prompts/micro/tool_preferences.md +1 -0
- package/prompts/micro/trust_subagent_context.md +1 -0
- package/prompts/micro/when_to_use_sub_agent.md +1 -0
- package/src/core/agentic.py +1 -73
- package/src/core/chat_manager.py +42 -7
- package/src/core/config_manager.py +6 -0
- package/src/core/cron.py +57 -2
- package/src/core/memory.py +3 -90
- package/src/llm/config.py +28 -2
- package/src/llm/prompts.py +251 -497
- package/src/llm/providers.py +25 -6
- package/src/llm/token_tracker.py +17 -1
- package/src/tools/edit.py +8 -6
- package/src/tools/helpers/path_resolver.py +18 -12
- package/src/tools/rg_search.py +97 -30
- package/src/ui/commands.py +120 -5
- package/src/ui/displays.py +1 -0
- package/src/ui/main.py +1 -0
- package/src/utils/settings.py +19 -2
- package/src/utils/user_message_logger.py +120 -0
package/src/core/agentic.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
import time
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Optional
|
|
8
7
|
|
|
@@ -172,63 +171,6 @@ class AgenticOrchestrator:
|
|
|
172
171
|
# Check if we're in a parallel context with suppressed console
|
|
173
172
|
return self._parallel_context.get('console', self.console)
|
|
174
173
|
|
|
175
|
-
def _is_memory_file(self, path: str) -> bool:
|
|
176
|
-
"""Check if path targets a memory file (auto-approved).
|
|
177
|
-
|
|
178
|
-
Auto-approve scope (restricted to known memory paths):
|
|
179
|
-
- {repo_root}/.bone/agents.md — project memory
|
|
180
|
-
- ~/.bone/user_memory.md — global user memory
|
|
181
|
-
- Any file under {repo_root}/.bone/ — project memory directory
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
path: File path from tool arguments.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
True if the file should be auto-approved as a memory file.
|
|
188
|
-
"""
|
|
189
|
-
p = Path(path).resolve()
|
|
190
|
-
repo_root = Path(self.repo_root).resolve()
|
|
191
|
-
# Known memory paths
|
|
192
|
-
if p == Path.home() / ".bone" / "user_memory.md":
|
|
193
|
-
return True
|
|
194
|
-
# Any file under {repo_root}/.bone/ (future memory files)
|
|
195
|
-
bone_dir = repo_root / ".bone"
|
|
196
|
-
if p.is_relative_to(bone_dir):
|
|
197
|
-
return True
|
|
198
|
-
return False
|
|
199
|
-
|
|
200
|
-
def _execute_memory_edit(self, arguments) -> bool:
|
|
201
|
-
"""Apply a memory file edit synchronously with one retry on failure.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
arguments: Tool arguments dict (path, search, replace, etc.)
|
|
205
|
-
"""
|
|
206
|
-
from tools.edit import _execute_edit_file
|
|
207
|
-
|
|
208
|
-
kwargs = dict(
|
|
209
|
-
path=arguments.get("path"),
|
|
210
|
-
search=arguments.get("search"),
|
|
211
|
-
replace=arguments.get("replace"),
|
|
212
|
-
repo_root=self.repo_root,
|
|
213
|
-
console=None, # silent — no output in chat
|
|
214
|
-
gitignore_spec=self.gitignore_spec,
|
|
215
|
-
context_lines=arguments.get("context_lines", 3),
|
|
216
|
-
vault_root=vault_root_str(),
|
|
217
|
-
)
|
|
218
|
-
try:
|
|
219
|
-
_execute_edit_file(**kwargs)
|
|
220
|
-
return True
|
|
221
|
-
except Exception as e:
|
|
222
|
-
logger.warning("Memory edit failed (retrying in 0.5s): %s", e)
|
|
223
|
-
time.sleep(0.5)
|
|
224
|
-
try:
|
|
225
|
-
_execute_edit_file(**kwargs)
|
|
226
|
-
logger.info("Memory edit retry succeeded after initial failure.")
|
|
227
|
-
return True
|
|
228
|
-
except Exception as e2:
|
|
229
|
-
logger.error("Memory edit failed after retry: %s", e2)
|
|
230
|
-
return False
|
|
231
|
-
|
|
232
174
|
def run(self, user_input, thinking_indicator=None, allowed_tools=None):
|
|
233
175
|
"""Main orchestration loop.
|
|
234
176
|
|
|
@@ -908,26 +850,12 @@ class AgenticOrchestrator:
|
|
|
908
850
|
|
|
909
851
|
# Check if tool requires approval
|
|
910
852
|
if tool.requires_approval:
|
|
911
|
-
# For edit_file:
|
|
853
|
+
# For edit_file: validate path then request approval
|
|
912
854
|
if function_name == "edit_file":
|
|
913
855
|
edit_path = arguments.get("path", "")
|
|
914
856
|
if not edit_path:
|
|
915
857
|
return False, "Error: path is required for edit_file."
|
|
916
858
|
|
|
917
|
-
# Memory file: auto-approve, fire-and-forget
|
|
918
|
-
if self._is_memory_file(edit_path):
|
|
919
|
-
# Generate preview to validate the edit (reuses existing logic)
|
|
920
|
-
result = tool.execute(arguments, context)
|
|
921
|
-
preview, is_valid = resolve_edit_preview(result)
|
|
922
|
-
if is_valid:
|
|
923
|
-
ok = self._execute_memory_edit(arguments)
|
|
924
|
-
if self.debug_mode:
|
|
925
|
-
console = self._get_console()
|
|
926
|
-
if console:
|
|
927
|
-
console.print(f"[dim]Memory edit auto-approved: {edit_path}[/dim]")
|
|
928
|
-
return False, "Memory saved." if ok else f"Memory edit failed: {edit_path}"
|
|
929
|
-
return False, str(result)
|
|
930
|
-
|
|
931
859
|
# Normal edit: generate preview and request approval
|
|
932
860
|
result = tool.execute(arguments, context)
|
|
933
861
|
|
package/src/core/chat_manager.py
CHANGED
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from llm.token_tracker import TokenTracker
|
|
16
16
|
from utils.settings import server_settings, context_settings
|
|
17
17
|
from utils.logger import MarkdownConversationLogger
|
|
18
|
+
from utils.user_message_logger import UserMessageLogger
|
|
18
19
|
from utils.result_parsers import extract_exit_code, extract_metadata_from_result
|
|
19
20
|
|
|
20
21
|
# Token counting constants
|
|
@@ -62,6 +63,9 @@ class ChatManager:
|
|
|
62
63
|
conversations_dir=context_settings.conversations_dir
|
|
63
64
|
)
|
|
64
65
|
|
|
66
|
+
# User message logging (always on, for dream memory system)
|
|
67
|
+
self.user_message_logger = UserMessageLogger()
|
|
68
|
+
|
|
65
69
|
# Compaction lock: prevents compaction during active tool execution
|
|
66
70
|
# Set by agentic.py before executing tools, cleared after all results appended
|
|
67
71
|
self._compaction_locked = False
|
|
@@ -119,20 +123,37 @@ class ChatManager:
|
|
|
119
123
|
self._update_context_tokens()
|
|
120
124
|
self.context_token_estimate = self.token_tracker.current_context_tokens
|
|
121
125
|
|
|
122
|
-
def _build_system_prompt(self) -> str:
|
|
123
|
-
"""Build system prompt.
|
|
124
|
-
|
|
126
|
+
def _build_system_prompt(self, variant: str | None = None) -> str:
|
|
127
|
+
"""Build system prompt.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
variant: Prompt variant name (e.g. 'main', 'micro').
|
|
131
|
+
If None, reads from prompt_settings.
|
|
132
|
+
"""
|
|
133
|
+
if variant is None:
|
|
134
|
+
from utils.settings import prompt_settings
|
|
135
|
+
variant = prompt_settings.variant
|
|
136
|
+
return build_system_prompt(variant)
|
|
137
|
+
|
|
138
|
+
def update_system_prompt(self, variant: str | None = None):
|
|
139
|
+
"""Rebuild system prompt in-place (e.g. after hotswap or session reset).
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
Args:
|
|
142
|
+
variant: Prompt variant to use. If None, keeps current variant.
|
|
143
|
+
Updates token_tracker.current_variant.
|
|
144
|
+
"""
|
|
128
145
|
if not self.messages:
|
|
129
146
|
raise RuntimeError("Cannot update system prompt: messages array is empty")
|
|
130
147
|
|
|
131
148
|
if self.messages[0]["role"] != "system":
|
|
132
149
|
raise RuntimeError(f"Cannot update system prompt: messages[0] has role '{self.messages[0]['role']}', expected 'system'")
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
if variant is None:
|
|
152
|
+
from utils.settings import prompt_settings
|
|
153
|
+
variant = prompt_settings.variant
|
|
154
|
+
|
|
155
|
+
self.messages[0]["content"] = self._build_system_prompt(variant)
|
|
156
|
+
self.token_tracker.current_variant = variant
|
|
136
157
|
self._update_context_tokens()
|
|
137
158
|
|
|
138
159
|
def _load_agents_md(self) -> tuple[str, str]:
|
|
@@ -1376,6 +1397,10 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1376
1397
|
server_path,
|
|
1377
1398
|
"-m", model_path,
|
|
1378
1399
|
"-ngl", str(server_settings.ngl_layers),
|
|
1400
|
+
"--threads", str(server_settings.threads),
|
|
1401
|
+
"--batch-size", str(server_settings.batch_size),
|
|
1402
|
+
"--ubatch-size", str(server_settings.ubatch_size),
|
|
1403
|
+
"--flash-attn" if server_settings.flash_attn else "--no-flash-attn",
|
|
1379
1404
|
"--split-mode", "none",
|
|
1380
1405
|
"--ctx-size", str(server_settings.ctx_size),
|
|
1381
1406
|
"--n-predict", str(server_settings.n_predict),
|
|
@@ -1383,6 +1408,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1383
1408
|
"--host", host,
|
|
1384
1409
|
"--port", str(port),
|
|
1385
1410
|
"--jinja",
|
|
1411
|
+
"--reasoning", "off",
|
|
1386
1412
|
]
|
|
1387
1413
|
|
|
1388
1414
|
# Restrict to RTX 5070 Ti only (GPU 0)
|
|
@@ -1459,6 +1485,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1459
1485
|
if self.markdown_logger:
|
|
1460
1486
|
self.markdown_logger.log_message(message)
|
|
1461
1487
|
|
|
1488
|
+
# Log user messages to JSONL for dream memory processing (only if memory enabled)
|
|
1489
|
+
if message.get("role") == "user" and message.get("content"):
|
|
1490
|
+
from llm.config import MEMORY_SETTINGS
|
|
1491
|
+
if MEMORY_SETTINGS.get("enabled", True):
|
|
1492
|
+
self.user_message_logger.log_user_message(
|
|
1493
|
+
message["content"],
|
|
1494
|
+
project_dir=Path.cwd().resolve(),
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1462
1497
|
def sync_log(self):
|
|
1463
1498
|
"""Rewrite the entire conversation log to match current message state.
|
|
1464
1499
|
|
|
@@ -116,9 +116,11 @@ class ConfigManager:
|
|
|
116
116
|
'bone': 'BONE_PROXY_MODEL',
|
|
117
117
|
'openrouter': 'OPENROUTER_MODEL',
|
|
118
118
|
'glm': 'GLM_MODEL',
|
|
119
|
+
'glm_plan': 'GLM_PLAN_MODEL',
|
|
119
120
|
'openai': 'OPENAI_MODEL',
|
|
120
121
|
'gemini': 'GEMINI_MODEL',
|
|
121
122
|
'minimax': 'MINIMAX_MODEL',
|
|
123
|
+
'minimax_plan': 'MINIMAX_PLAN_MODEL',
|
|
122
124
|
'anthropic': 'ANTHROPIC_MODEL',
|
|
123
125
|
'kimi': 'KIMI_MODEL'
|
|
124
126
|
}
|
|
@@ -144,9 +146,11 @@ class ConfigManager:
|
|
|
144
146
|
'bone': 'BONE_PROXY_MODEL',
|
|
145
147
|
'openrouter': 'OPENROUTER_MODEL',
|
|
146
148
|
'glm': 'GLM_MODEL',
|
|
149
|
+
'glm_plan': 'GLM_PLAN_MODEL',
|
|
147
150
|
'openai': 'OPENAI_MODEL',
|
|
148
151
|
'gemini': 'GEMINI_MODEL',
|
|
149
152
|
'minimax': 'MINIMAX_MODEL',
|
|
153
|
+
'minimax_plan': 'MINIMAX_PLAN_MODEL',
|
|
150
154
|
'anthropic': 'ANTHROPIC_MODEL',
|
|
151
155
|
'kimi': 'KIMI_MODEL'
|
|
152
156
|
}
|
|
@@ -172,9 +176,11 @@ class ConfigManager:
|
|
|
172
176
|
'openrouter': 'OPENROUTER_API_KEY',
|
|
173
177
|
'bone': 'BONE_PROXY_API_KEY',
|
|
174
178
|
'glm': 'GLM_API_KEY',
|
|
179
|
+
'glm_plan': 'GLM_PLAN_API_KEY',
|
|
175
180
|
'openai': 'OPENAI_API_KEY',
|
|
176
181
|
'gemini': 'GEMINI_API_KEY',
|
|
177
182
|
'minimax': 'MINIMAX_API_KEY',
|
|
183
|
+
'minimax_plan': 'MINIMAX_PLAN_API_KEY',
|
|
178
184
|
'anthropic': 'ANTHROPIC_API_KEY',
|
|
179
185
|
'kimi': 'KIMI_API_KEY'
|
|
180
186
|
}
|
package/src/core/cron.py
CHANGED
|
@@ -273,6 +273,41 @@ def _write_job_log(job: CronJob, output: str, error: bool):
|
|
|
273
273
|
logger.error("Failed to write cron log: %s", e)
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
# ── Dream job (auto-seeded) ─────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
DREAM_JOB_ID = "dream"
|
|
279
|
+
DREAM_JOB_SCHEDULE = "daily at 4am"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def ensure_dream_job(config: CronConfig) -> None:
|
|
283
|
+
"""Sync the dream memory job with the DREAM_SETTINGS.enabled config.
|
|
284
|
+
|
|
285
|
+
- Enabled and missing → seed the job
|
|
286
|
+
- Enabled and present → no-op
|
|
287
|
+
- Disabled and present → remove the job
|
|
288
|
+
- Disabled and missing → no-op
|
|
289
|
+
"""
|
|
290
|
+
from utils.settings import dream_settings
|
|
291
|
+
from llm.config import MEMORY_SETTINGS
|
|
292
|
+
|
|
293
|
+
if dream_settings.enabled and MEMORY_SETTINGS.get("enabled", True):
|
|
294
|
+
if DREAM_JOB_ID in config.jobs:
|
|
295
|
+
return
|
|
296
|
+
job = CronJob(
|
|
297
|
+
id=DREAM_JOB_ID,
|
|
298
|
+
schedule=DREAM_JOB_SCHEDULE,
|
|
299
|
+
command="Run the dream memory consolidation process. Read yesterday's user messages from ~/.bone/conversations/, analyze them for preferences and patterns, and consolidate into memory files. Then clean up JSONL files older than 7 days.",
|
|
300
|
+
enabled=True,
|
|
301
|
+
description="Dream memory consolidation — scans user messages and updates memories",
|
|
302
|
+
)
|
|
303
|
+
config.add_job(job)
|
|
304
|
+
logger.info("Seeded dream memory cron job (daily at 4am)")
|
|
305
|
+
else:
|
|
306
|
+
if DREAM_JOB_ID in config.jobs:
|
|
307
|
+
config.remove_job(DREAM_JOB_ID)
|
|
308
|
+
logger.info("Removed dream memory cron job (disabled in config)")
|
|
309
|
+
|
|
310
|
+
|
|
276
311
|
def run_single_job(job: CronJob, console=None, interactive=False) -> None:
|
|
277
312
|
"""Execute a single cron job without requiring a CronScheduler instance.
|
|
278
313
|
|
|
@@ -321,10 +356,27 @@ def run_single_job(job: CronJob, console=None, interactive=False) -> None:
|
|
|
321
356
|
# Fresh ChatManager for this job
|
|
322
357
|
chat_manager = ChatManager()
|
|
323
358
|
|
|
324
|
-
#
|
|
359
|
+
# Dream job: auto-approve edits and run cleanup before agent starts
|
|
360
|
+
if job.id == DREAM_JOB_ID:
|
|
361
|
+
chat_manager.approve_mode = "accept_edits"
|
|
362
|
+
from utils.user_message_logger import UserMessageLogger
|
|
363
|
+
removed = UserMessageLogger.cleanup_old_files()
|
|
364
|
+
if removed:
|
|
365
|
+
logger.info("Dream job: removed %d old JSONL files", removed)
|
|
366
|
+
|
|
367
|
+
# Build the prompt — load dream.md for dream job, else use command field
|
|
368
|
+
if job.id == DREAM_JOB_ID:
|
|
369
|
+
dream_prompt_path = Path(__file__).resolve().parents[2] / "prompts" / "main" / "dream.md"
|
|
370
|
+
if dream_prompt_path.is_file():
|
|
371
|
+
command_text = dream_prompt_path.read_text(encoding="utf-8").strip()
|
|
372
|
+
else:
|
|
373
|
+
command_text = job.command
|
|
374
|
+
else:
|
|
375
|
+
command_text = job.command
|
|
376
|
+
|
|
325
377
|
prompt = (
|
|
326
378
|
f"[Cron job: {job.id}]\n"
|
|
327
|
-
f"{
|
|
379
|
+
f"{command_text}"
|
|
328
380
|
)
|
|
329
381
|
|
|
330
382
|
repo_root = Path.cwd().resolve()
|
|
@@ -372,6 +424,9 @@ class CronScheduler:
|
|
|
372
424
|
self._lock = threading.Lock()
|
|
373
425
|
self._running = False
|
|
374
426
|
|
|
427
|
+
# Auto-seed the dream memory job if it doesn't exist
|
|
428
|
+
ensure_dream_job(self.config)
|
|
429
|
+
|
|
375
430
|
def start(self):
|
|
376
431
|
"""Start the cron scheduler background thread."""
|
|
377
432
|
enabled_jobs = [j for j in self.config.jobs.values() if j.enabled]
|
package/src/core/memory.py
CHANGED
|
@@ -4,8 +4,9 @@ Two-layer persistent memory:
|
|
|
4
4
|
- User memory (global): ~/.bone/user_memory.md
|
|
5
5
|
- Project memory (per-repo): {repo_root}/.bone/agents.md
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Memory files are read-only during conversations — loaded into the system prompt
|
|
8
|
+
for context but never written inline. All writes happen through the dream cron job,
|
|
9
|
+
which consolidates user messages into focused memories nightly.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
import logging
|
|
@@ -16,8 +17,6 @@ logger = logging.getLogger(__name__)
|
|
|
16
17
|
|
|
17
18
|
# Capacity constants (prompt-enforced, no code enforcement)
|
|
18
19
|
CHAR_LIMIT = 1500 # suggested chars per layer (~500 tokens)
|
|
19
|
-
SECTION_LIMIT = 8 # suggested max sections per layer
|
|
20
|
-
ENTRY_LIMIT = 20 # suggested max entries per section
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class MemoryManager:
|
|
@@ -80,17 +79,6 @@ class MemoryManager:
|
|
|
80
79
|
"""Read and return project memory file content. Returns empty string if missing."""
|
|
81
80
|
return self._read_file(self.project_memory_path)
|
|
82
81
|
|
|
83
|
-
def load_all(self) -> str:
|
|
84
|
-
"""Load both layers, combined for prompt injection."""
|
|
85
|
-
parts = []
|
|
86
|
-
user = self.load_user_memory()
|
|
87
|
-
project = self.load_project_memory()
|
|
88
|
-
if user.strip():
|
|
89
|
-
parts.append(user.strip())
|
|
90
|
-
if project.strip():
|
|
91
|
-
parts.append(project.strip())
|
|
92
|
-
return "\n\n".join(parts)
|
|
93
|
-
|
|
94
82
|
def get_user_usage(self) -> dict:
|
|
95
83
|
"""Return {chars_used, chars_limit} for user memory."""
|
|
96
84
|
content = self.load_user_memory()
|
|
@@ -101,81 +89,6 @@ class MemoryManager:
|
|
|
101
89
|
content = self.load_project_memory()
|
|
102
90
|
return {"chars_used": len(content), "chars_limit": CHAR_LIMIT}
|
|
103
91
|
|
|
104
|
-
def get_prompt_section(self) -> str:
|
|
105
|
-
"""Build the full memory system prompt section.
|
|
106
|
-
|
|
107
|
-
Includes:
|
|
108
|
-
- Guidelines text with resolved file paths
|
|
109
|
-
- Capacity headers and memory content (if files have entries beyond headers)
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Complete prompt section string. Includes guidelines even when
|
|
113
|
-
memory files are empty (just headers). Returns guidelines with
|
|
114
|
-
placeholder paths if no MemoryManager instance exists.
|
|
115
|
-
"""
|
|
116
|
-
user_path = str(self.user_memory_path)
|
|
117
|
-
project_path = str(self.project_memory_path)
|
|
118
|
-
|
|
119
|
-
lines = [
|
|
120
|
-
"## Memory System",
|
|
121
|
-
"",
|
|
122
|
-
"You have a two-layer memory system that persists across conversations:",
|
|
123
|
-
f"- User memory (global): {user_path} — preferences, identity, work patterns",
|
|
124
|
-
f"- Project memory (per-repo): {project_path} — context, conventions, decisions, current work",
|
|
125
|
-
"",
|
|
126
|
-
"Both memory layers are loaded into this prompt at conversation start. "
|
|
127
|
-
"You can already see all memories below.",
|
|
128
|
-
"",
|
|
129
|
-
"To save information, use `edit_file` to write directly to the memory files. "
|
|
130
|
-
"These edits are auto-approved and run silently.",
|
|
131
|
-
"Add a timestamp in parentheses: `*(YYYY-MM-DD)*`",
|
|
132
|
-
"",
|
|
133
|
-
"### Save these (proactively):",
|
|
134
|
-
"- User preferences: \"I prefer TypeScript over JavaScript\" → user memory",
|
|
135
|
-
"- Environment facts: \"This project uses Python 3.11 with pytest\" → project memory",
|
|
136
|
-
"- Corrections: \"Don't use sudo for docker, user is in docker group\" → project memory",
|
|
137
|
-
"- Conventions: \"Project uses tabs, 120-char line width\" → project memory",
|
|
138
|
-
"- Completed work: \"Migrated database schema on 2026-04-20\" → project memory",
|
|
139
|
-
"- Explicit requests: \"Remember that my API key rotation happens monthly\" → user memory",
|
|
140
|
-
"",
|
|
141
|
-
"### Skip these:",
|
|
142
|
-
"- Trivial/obvious info: \"User asked about Python\" — too vague to be useful",
|
|
143
|
-
"- Easily re-discovered facts: \"Python 3.12 supports f-string nesting\" — can web search this",
|
|
144
|
-
"- Raw data dumps: Large code blocks, log files, data tables — too big for memory",
|
|
145
|
-
"- Session-specific ephemera: Temporary file paths, one-off debugging context",
|
|
146
|
-
"- Information already in agents.md or other context files",
|
|
147
|
-
"",
|
|
148
|
-
"Keep memories concise and information-dense. Use the section that best fits the information.",
|
|
149
|
-
"To update a memory, edit the entry in place with a new timestamp.",
|
|
150
|
-
"To remove a memory, delete the line.",
|
|
151
|
-
f"Stay under {CHAR_LIMIT} chars per file (~500 tokens). "
|
|
152
|
-
f"When above 80% ({int(CHAR_LIMIT * 0.8)} chars), consolidate older entries before adding new ones.",
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
# Add capacity headers and memory content if files have real content
|
|
156
|
-
user_content = self.load_user_memory()
|
|
157
|
-
user_usage = self.get_user_usage()
|
|
158
|
-
# Only show block if file has more than just the header
|
|
159
|
-
if self._has_entries(user_content):
|
|
160
|
-
pct = user_usage["chars_used"] * 100 // user_usage["chars_limit"]
|
|
161
|
-
lines.extend([
|
|
162
|
-
"",
|
|
163
|
-
f"USER MEMORY [{pct}% — {user_usage['chars_used']}/{user_usage['chars_limit']} chars]",
|
|
164
|
-
user_content.strip(),
|
|
165
|
-
])
|
|
166
|
-
|
|
167
|
-
project_content = self.load_project_memory()
|
|
168
|
-
project_usage = self.get_project_usage()
|
|
169
|
-
if self._has_entries(project_content):
|
|
170
|
-
pct = project_usage["chars_used"] * 100 // project_usage["chars_limit"]
|
|
171
|
-
lines.extend([
|
|
172
|
-
"",
|
|
173
|
-
f"PROJECT MEMORY [{pct}% — {project_usage['chars_used']}/{project_usage['chars_limit']} chars]",
|
|
174
|
-
project_content.strip(),
|
|
175
|
-
])
|
|
176
|
-
|
|
177
|
-
return "\n".join(lines)
|
|
178
|
-
|
|
179
92
|
# ---- Private helpers ----
|
|
180
93
|
|
|
181
94
|
@staticmethod
|
package/src/llm/config.py
CHANGED
|
@@ -354,7 +354,7 @@ def reload_config():
|
|
|
354
354
|
|
|
355
355
|
Note: This is a manual operation - call after config changes.
|
|
356
356
|
"""
|
|
357
|
-
global _CONFIG, _provider_registry_cache, _cached_provider, PROVIDER_REGISTRY, LLM_PROVIDER, STATUS_BAR_SETTINGS
|
|
357
|
+
global _CONFIG, _provider_registry_cache, _cached_provider, PROVIDER_REGISTRY, LLM_PROVIDER, STATUS_BAR_SETTINGS, MEMORY_SETTINGS
|
|
358
358
|
_CONFIG = _load_config()
|
|
359
359
|
_provider_registry_cache = None
|
|
360
360
|
_cached_provider = None
|
|
@@ -363,6 +363,8 @@ def reload_config():
|
|
|
363
363
|
LLM_PROVIDER = _get_provider()
|
|
364
364
|
# Rebuild status bar settings
|
|
365
365
|
STATUS_BAR_SETTINGS = _build_status_bar_settings()
|
|
366
|
+
# Rebuild memory settings
|
|
367
|
+
MEMORY_SETTINGS = _build_memory_settings()
|
|
366
368
|
|
|
367
369
|
|
|
368
370
|
def _build_status_bar_settings():
|
|
@@ -377,8 +379,27 @@ def _build_status_bar_settings():
|
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
|
|
382
|
+
def _build_memory_settings():
|
|
383
|
+
"""Build MEMORY_SETTINGS dict from current _CONFIG."""
|
|
384
|
+
ms = _CONFIG.get("MEMORY_SETTINGS", {})
|
|
385
|
+
return {
|
|
386
|
+
"enabled": ms.get("enabled", True),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def update_memory_settings(settings_dict):
|
|
391
|
+
"""Update MEMORY_SETTINGS at runtime.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Updated MEMORY_SETTINGS dict
|
|
395
|
+
"""
|
|
396
|
+
global MEMORY_SETTINGS
|
|
397
|
+
MEMORY_SETTINGS.update(settings_dict)
|
|
398
|
+
return MEMORY_SETTINGS
|
|
399
|
+
|
|
400
|
+
|
|
380
401
|
def update_status_bar_settings(settings_dict):
|
|
381
|
-
"""Update STATUS_BAR_SETTINGS at runtime
|
|
402
|
+
"""Update STATUS_BAR_SETTINGS at runtime.
|
|
382
403
|
|
|
383
404
|
Args:
|
|
384
405
|
settings_dict: Dict of settings to update (e.g., {"show_cost": False})
|
|
@@ -426,6 +447,8 @@ __all__ = [
|
|
|
426
447
|
"reload_config",
|
|
427
448
|
"STATUS_BAR_SETTINGS",
|
|
428
449
|
"update_status_bar_settings",
|
|
450
|
+
"MEMORY_SETTINGS",
|
|
451
|
+
"update_memory_settings",
|
|
429
452
|
]
|
|
430
453
|
|
|
431
454
|
|
|
@@ -445,6 +468,9 @@ WEB_SEARCH_REQUIRE_CONFIRMATION = False
|
|
|
445
468
|
# Status bar configuration
|
|
446
469
|
STATUS_BAR_SETTINGS = _build_status_bar_settings()
|
|
447
470
|
|
|
471
|
+
# Memory configuration
|
|
472
|
+
MEMORY_SETTINGS = _build_memory_settings()
|
|
473
|
+
|
|
448
474
|
# Tool approval modes
|
|
449
475
|
APPROVE_MODES = ("safe", "accept_edits", "danger")
|
|
450
476
|
CYCLEABLE_APPROVE_MODES = ("safe", "accept_edits")
|