bone-agent 1.3.1 → 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 +9 -78
- package/src/core/chat_manager.py +120 -108
- 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/tools/select_option.py +12 -5
- 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
|
|
|
@@ -380,7 +322,7 @@ class AgenticOrchestrator:
|
|
|
380
322
|
self.chat_manager.log_message(response)
|
|
381
323
|
|
|
382
324
|
# NEW: Compact tool results after final answer (per-message compaction)
|
|
383
|
-
self.chat_manager.compact_tool_results()
|
|
325
|
+
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
384
326
|
|
|
385
327
|
# Update context tokens with current mode's tools
|
|
386
328
|
tools_for_mode = TOOLS()
|
|
@@ -597,9 +539,8 @@ class AgenticOrchestrator:
|
|
|
597
539
|
# Log tool result
|
|
598
540
|
self.chat_manager.log_message(tool_msg)
|
|
599
541
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
self.chat_manager.compact_tool_results()
|
|
542
|
+
# Compact completed tool blocks once after all tools complete
|
|
543
|
+
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
603
544
|
|
|
604
545
|
# Update context tokens with current mode's tools
|
|
605
546
|
tools_for_mode = TOOLS()
|
|
@@ -840,7 +781,7 @@ class AgenticOrchestrator:
|
|
|
840
781
|
|
|
841
782
|
# Mid-loop compaction: compact older completed tool blocks
|
|
842
783
|
# after all parallel results are appended (safe — only compacts completed blocks)
|
|
843
|
-
self.chat_manager.compact_tool_results()
|
|
784
|
+
self.chat_manager.compact_tool_results(skip_token_update=True)
|
|
844
785
|
|
|
845
786
|
# Update context tokens with current mode's tools
|
|
846
787
|
tools_for_mode = TOOLS()
|
|
@@ -909,26 +850,12 @@ class AgenticOrchestrator:
|
|
|
909
850
|
|
|
910
851
|
# Check if tool requires approval
|
|
911
852
|
if tool.requires_approval:
|
|
912
|
-
# For edit_file:
|
|
853
|
+
# For edit_file: validate path then request approval
|
|
913
854
|
if function_name == "edit_file":
|
|
914
855
|
edit_path = arguments.get("path", "")
|
|
915
856
|
if not edit_path:
|
|
916
857
|
return False, "Error: path is required for edit_file."
|
|
917
858
|
|
|
918
|
-
# Memory file: auto-approve, fire-and-forget
|
|
919
|
-
if self._is_memory_file(edit_path):
|
|
920
|
-
# Generate preview to validate the edit (reuses existing logic)
|
|
921
|
-
result = tool.execute(arguments, context)
|
|
922
|
-
preview, is_valid = resolve_edit_preview(result)
|
|
923
|
-
if is_valid:
|
|
924
|
-
ok = self._execute_memory_edit(arguments)
|
|
925
|
-
if self.debug_mode:
|
|
926
|
-
console = self._get_console()
|
|
927
|
-
if console:
|
|
928
|
-
console.print(f"[dim]Memory edit auto-approved: {edit_path}[/dim]")
|
|
929
|
-
return False, "Memory saved." if ok else f"Memory edit failed: {edit_path}"
|
|
930
|
-
return False, str(result)
|
|
931
|
-
|
|
932
859
|
# Normal edit: generate preview and request approval
|
|
933
860
|
result = tool.execute(arguments, context)
|
|
934
861
|
|
|
@@ -1023,6 +950,10 @@ class AgenticOrchestrator:
|
|
|
1023
950
|
|
|
1024
951
|
return False, str(result)
|
|
1025
952
|
except Exception as e:
|
|
953
|
+
# If thinking_indicator was paused (TERMINAL_YIELD) and tool
|
|
954
|
+
# raised, resume it so the spinner reappears for the next iteration
|
|
955
|
+
if policy == TERMINAL_YIELD and thinking_indicator:
|
|
956
|
+
thinking_indicator.resume()
|
|
1026
957
|
return False, f"Error executing tool '{function_name}': {str(e)}"
|
|
1027
958
|
|
|
1028
959
|
return False, f"Error: Unknown tool '{function_name}'."
|
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]:
|
|
@@ -383,7 +404,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
383
404
|
|
|
384
405
|
# ===== Tool Result Compaction =====
|
|
385
406
|
|
|
386
|
-
def _find_tool_blocks(self):
|
|
407
|
+
def _find_tool_blocks(self, include_in_flight=False):
|
|
387
408
|
"""Find all tool-result blocks in message history.
|
|
388
409
|
|
|
389
410
|
Handles both single-turn and multi-turn tool chains:
|
|
@@ -394,6 +415,12 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
394
415
|
a single block spanning from the first assistant(tool_calls) to the
|
|
395
416
|
final assistant(answer).
|
|
396
417
|
|
|
418
|
+
Args:
|
|
419
|
+
include_in_flight: If True, also return blocks that lack a final
|
|
420
|
+
assistant answer (in-flight tool chains). The 'end' field points
|
|
421
|
+
to the index after the last message in the chain (or the breaking
|
|
422
|
+
message index if the chain was interrupted).
|
|
423
|
+
|
|
397
424
|
Returns:
|
|
398
425
|
list: List of block dicts with keys: user_idx, start, end, tool_calls, tool_results
|
|
399
426
|
"""
|
|
@@ -441,14 +468,25 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
441
468
|
# Non-tool, non-assistant message breaks the chain
|
|
442
469
|
break
|
|
443
470
|
|
|
444
|
-
if
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
471
|
+
if include_in_flight:
|
|
472
|
+
if all_tool_calls:
|
|
473
|
+
blocks.append({
|
|
474
|
+
'user_idx': user_idx,
|
|
475
|
+
'start': block_start,
|
|
476
|
+
'end': j,
|
|
477
|
+
'tool_calls': all_tool_calls,
|
|
478
|
+
'tool_results': all_tool_results,
|
|
479
|
+
'in_flight': not found_end,
|
|
480
|
+
})
|
|
481
|
+
else:
|
|
482
|
+
if found_end and all_tool_calls:
|
|
483
|
+
blocks.append({
|
|
484
|
+
'user_idx': user_idx,
|
|
485
|
+
'start': block_start,
|
|
486
|
+
'end': j,
|
|
487
|
+
'tool_calls': all_tool_calls,
|
|
488
|
+
'tool_results': all_tool_results,
|
|
489
|
+
})
|
|
452
490
|
|
|
453
491
|
# Continue scanning from after the final answer (or after the chain)
|
|
454
492
|
# Guard: always advance at least one position to prevent infinite loops
|
|
@@ -635,68 +673,21 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
635
673
|
def _find_in_flight_boundary(self):
|
|
636
674
|
"""Find the index where in-flight tool blocks begin.
|
|
637
675
|
|
|
638
|
-
|
|
639
|
-
|
|
676
|
+
Delegates to _find_tool_blocks(include_in_flight=True) to find all
|
|
677
|
+
blocks, then returns the earliest start of any in-flight block.
|
|
640
678
|
These messages must never be included in the compactable region.
|
|
641
679
|
|
|
642
680
|
Returns:
|
|
643
681
|
int: Index of the first in-flight message, or len(messages) if none.
|
|
644
682
|
"""
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
# Found an assistant with tool_calls. Check if there's a final
|
|
654
|
-
# answer (assistant without tool_calls) after it.
|
|
655
|
-
has_final_answer = False
|
|
656
|
-
j = i + 1
|
|
657
|
-
while j < n:
|
|
658
|
-
if self.messages[j].get('role') == 'assistant' and not self.messages[j].get('tool_calls'):
|
|
659
|
-
has_final_answer = True
|
|
660
|
-
break
|
|
661
|
-
elif self.messages[j].get('role') == 'assistant' and self.messages[j].get('tool_calls'):
|
|
662
|
-
# Another tool-calling assistant — skip over its tool results
|
|
663
|
-
j += 1
|
|
664
|
-
while j < n and self.messages[j].get('role') == 'tool':
|
|
665
|
-
j += 1
|
|
666
|
-
continue
|
|
667
|
-
elif self.messages[j].get('role') == 'tool':
|
|
668
|
-
j += 1
|
|
669
|
-
continue
|
|
670
|
-
else:
|
|
671
|
-
break
|
|
672
|
-
|
|
673
|
-
if not has_final_answer:
|
|
674
|
-
# This is an in-flight tool block. Find its user question.
|
|
675
|
-
user_idx = i - 1
|
|
676
|
-
while user_idx >= 0 and self.messages[user_idx].get('role') != 'user':
|
|
677
|
-
user_idx -= 1
|
|
678
|
-
return max(0, user_idx)
|
|
679
|
-
else:
|
|
680
|
-
# Completed block — continue scanning backward
|
|
681
|
-
# Skip past all the tool messages associated with this block
|
|
682
|
-
j = i + 1
|
|
683
|
-
while j < n:
|
|
684
|
-
if self.messages[j].get('role') == 'tool':
|
|
685
|
-
j += 1
|
|
686
|
-
elif self.messages[j].get('role') == 'assistant' and self.messages[j].get('tool_calls'):
|
|
687
|
-
j += 1
|
|
688
|
-
while j < n and self.messages[j].get('role') == 'tool':
|
|
689
|
-
j += 1
|
|
690
|
-
continue
|
|
691
|
-
else:
|
|
692
|
-
break
|
|
693
|
-
i = j - 1
|
|
694
|
-
else:
|
|
695
|
-
i -= 1
|
|
696
|
-
|
|
697
|
-
return n
|
|
698
|
-
|
|
699
|
-
def _compute_split_boundary(self, blocks, in_flight_start):
|
|
683
|
+
all_blocks = self._find_tool_blocks(include_in_flight=True)
|
|
684
|
+
in_flight = [b for b in all_blocks if b.get('in_flight')]
|
|
685
|
+
if in_flight:
|
|
686
|
+
return min(b['user_idx'] for b in in_flight)
|
|
687
|
+
return len(self.messages)
|
|
688
|
+
|
|
689
|
+
def _compute_split_boundary(self, blocks, in_flight_start,
|
|
690
|
+
uncompacted_tail_tokens=None, min_tool_blocks=None):
|
|
700
691
|
"""Compute the message index where the uncompacted tail begins.
|
|
701
692
|
|
|
702
693
|
Three constraints determine the boundary (take the most conservative /
|
|
@@ -709,19 +700,23 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
709
700
|
Args:
|
|
710
701
|
blocks: List of tool block dicts from _find_tool_blocks()
|
|
711
702
|
in_flight_start: Index of first in-flight message (from _find_in_flight_boundary)
|
|
703
|
+
uncompacted_tail_tokens: Override for the token budget (None = use settings)
|
|
704
|
+
min_tool_blocks: Override for minimum tool blocks to preserve (None = use settings)
|
|
712
705
|
|
|
713
706
|
Returns:
|
|
714
707
|
int: Message index where the uncompacted tail starts
|
|
715
708
|
"""
|
|
716
709
|
tc = context_settings.tool_compaction
|
|
717
|
-
token_budget = tc.uncompacted_tail_tokens
|
|
718
|
-
min_blocks = tc.min_tool_blocks
|
|
710
|
+
token_budget = uncompacted_tail_tokens if uncompacted_tail_tokens is not None else tc.uncompacted_tail_tokens
|
|
711
|
+
min_blocks = min_tool_blocks if min_tool_blocks is not None else tc.min_tool_blocks
|
|
719
712
|
n = len(self.messages)
|
|
720
713
|
|
|
721
714
|
# The verbatim region ends at the first in-flight message (exclusive)
|
|
722
715
|
verbatim_end = min(in_flight_start, n)
|
|
723
716
|
|
|
724
|
-
# Constraint 1: Token budget — walk from verbatim_end backward
|
|
717
|
+
# Constraint 1: Token budget — walk from verbatim_end backward.
|
|
718
|
+
# Note: range stops at 1 (not 0) so the system prompt is never counted
|
|
719
|
+
# toward the budget — it is always preserved uncompacted.
|
|
725
720
|
tokens_accumulated = 0
|
|
726
721
|
token_boundary = 0
|
|
727
722
|
for i in range(verbatim_end - 1, 0, -1):
|
|
@@ -734,25 +729,16 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
734
729
|
token_boundary = 1
|
|
735
730
|
|
|
736
731
|
# Constraint 2: Minimum tool blocks — ensure at least min_blocks completed
|
|
737
|
-
# blocks are within the
|
|
732
|
+
# blocks are within the uncompacted tail. Take the min_blocks most recent
|
|
733
|
+
# completed blocks and set the boundary so they all fall at or after it.
|
|
738
734
|
min_block_boundary = 1
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
# Need to extend backward to include more blocks
|
|
747
|
-
blocks_needed = min_blocks - len(completed_blocks_in_tail)
|
|
748
|
-
# Take the blocks immediately before the current tail
|
|
749
|
-
# Find blocks whose end < token_boundary (not already in tail)
|
|
750
|
-
earlier_blocks = [b for b in blocks if b['end'] < token_boundary]
|
|
751
|
-
# Sort by end index descending (most recent first)
|
|
752
|
-
earlier_blocks.sort(key=lambda b: b['end'], reverse=True)
|
|
753
|
-
# Extend boundary to include the earliest user_idx of the blocks we need
|
|
754
|
-
for b in earlier_blocks[:blocks_needed]:
|
|
755
|
-
min_block_boundary = min(min_block_boundary, b['user_idx'])
|
|
735
|
+
if min_blocks > 0 and len(blocks) >= min_blocks:
|
|
736
|
+
# Sort by end index descending (most recent first), take top min_blocks
|
|
737
|
+
sorted_blocks = sorted(blocks, key=lambda b: b['end'], reverse=True)
|
|
738
|
+
recent_blocks = sorted_blocks[:min_blocks]
|
|
739
|
+
# The boundary must be at or before the earliest user_idx of these blocks
|
|
740
|
+
# so that all of them satisfy user_idx >= boundary (i.e. block is fully in the tail)
|
|
741
|
+
min_block_boundary = min(b['user_idx'] for b in recent_blocks)
|
|
756
742
|
|
|
757
743
|
# Constraint 3: Tool-call integrity — if token_boundary lands inside a
|
|
758
744
|
# tool block, extend backward to include the complete block
|
|
@@ -763,13 +749,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
763
749
|
integrity_boundary = min(integrity_boundary, block['user_idx'])
|
|
764
750
|
|
|
765
751
|
# Take the most conservative (earliest) boundary
|
|
766
|
-
|
|
752
|
+
# integrity_boundary <= token_boundary always (starts equal, only decreases)
|
|
753
|
+
boundary = integrity_boundary
|
|
767
754
|
if min_block_boundary < boundary:
|
|
768
755
|
boundary = min_block_boundary
|
|
769
756
|
|
|
770
757
|
return boundary
|
|
771
758
|
|
|
772
|
-
def compact_tool_results(self
|
|
759
|
+
def compact_tool_results(self, skip_token_update=False,
|
|
760
|
+
uncompacted_tail_tokens=None, min_tool_blocks=None):
|
|
773
761
|
"""Replace completed tool-result blocks with summaries using token-budget tail.
|
|
774
762
|
|
|
775
763
|
Walks messages from the end, accumulating tokens until ~40k tokens are
|
|
@@ -779,6 +767,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
779
767
|
|
|
780
768
|
Safe to call mid-loop (during tool execution) because it only compacts
|
|
781
769
|
completed tool blocks — in-flight blocks are never touched.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
skip_token_update: If True, skip the internal _update_context_tokens()
|
|
773
|
+
call. Use when the caller will update tokens with mode-specific
|
|
774
|
+
tools immediately after.
|
|
775
|
+
uncompacted_tail_tokens: Override for the token budget (None = use settings).
|
|
776
|
+
Use for aggressive compaction with a smaller tail.
|
|
777
|
+
min_tool_blocks: Override for minimum tool blocks to preserve (None = use settings).
|
|
778
|
+
Use for aggressive compaction with fewer preserved blocks.
|
|
782
779
|
"""
|
|
783
780
|
# Skip if disabled (e.g. sub-agents preserving findings)
|
|
784
781
|
if self._compaction_disabled:
|
|
@@ -801,7 +798,11 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
801
798
|
in_flight_start = self._find_in_flight_boundary()
|
|
802
799
|
|
|
803
800
|
# Compute the split boundary using token budget + constraints
|
|
804
|
-
split_boundary = self._compute_split_boundary(
|
|
801
|
+
split_boundary = self._compute_split_boundary(
|
|
802
|
+
blocks, in_flight_start,
|
|
803
|
+
uncompacted_tail_tokens=uncompacted_tail_tokens,
|
|
804
|
+
min_tool_blocks=min_tool_blocks,
|
|
805
|
+
)
|
|
805
806
|
|
|
806
807
|
# Determine which blocks fall entirely before the split boundary
|
|
807
808
|
# (those are the ones to compact)
|
|
@@ -864,7 +865,8 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
864
865
|
new_messages.append(msg)
|
|
865
866
|
|
|
866
867
|
self.messages = new_messages
|
|
867
|
-
|
|
868
|
+
if not skip_token_update:
|
|
869
|
+
self._update_context_tokens()
|
|
868
870
|
|
|
869
871
|
# ===== AI-Based History Compaction =====
|
|
870
872
|
|
|
@@ -1101,16 +1103,12 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1101
1103
|
# If compaction is NOT locked, try layers 1 and 2
|
|
1102
1104
|
if not self._compaction_locked:
|
|
1103
1105
|
# Layer 1: Aggressive tool result compaction (non-LLM, fast)
|
|
1104
|
-
#
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
self.compact_tool_results()
|
|
1111
|
-
finally:
|
|
1112
|
-
context_settings.tool_compaction.uncompacted_tail_tokens = original_tail_tokens
|
|
1113
|
-
context_settings.tool_compaction.min_tool_blocks = original_min_blocks
|
|
1106
|
+
# Use very small token budget and min blocks for aggressive compaction
|
|
1107
|
+
self.compact_tool_results(
|
|
1108
|
+
skip_token_update=True,
|
|
1109
|
+
uncompacted_tail_tokens=10_000,
|
|
1110
|
+
min_tool_blocks=1,
|
|
1111
|
+
)
|
|
1114
1112
|
|
|
1115
1113
|
self._update_context_tokens()
|
|
1116
1114
|
current_tokens = self.token_tracker.current_context_tokens
|
|
@@ -1399,6 +1397,10 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1399
1397
|
server_path,
|
|
1400
1398
|
"-m", model_path,
|
|
1401
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",
|
|
1402
1404
|
"--split-mode", "none",
|
|
1403
1405
|
"--ctx-size", str(server_settings.ctx_size),
|
|
1404
1406
|
"--n-predict", str(server_settings.n_predict),
|
|
@@ -1406,6 +1408,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1406
1408
|
"--host", host,
|
|
1407
1409
|
"--port", str(port),
|
|
1408
1410
|
"--jinja",
|
|
1411
|
+
"--reasoning", "off",
|
|
1409
1412
|
]
|
|
1410
1413
|
|
|
1411
1414
|
# Restrict to RTX 5070 Ti only (GPU 0)
|
|
@@ -1482,6 +1485,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
|
|
|
1482
1485
|
if self.markdown_logger:
|
|
1483
1486
|
self.markdown_logger.log_message(message)
|
|
1484
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
|
+
|
|
1485
1497
|
def sync_log(self):
|
|
1486
1498
|
"""Rewrite the entire conversation log to match current message state.
|
|
1487
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]
|