bone-agent 1.3.2 → 1.4.0
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 +19 -2
- package/config.yaml.example +13 -2
- 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 +50 -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/skills.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/skills.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 +134 -106
- package/src/core/chat_manager.py +60 -12
- package/src/core/config_manager.py +14 -1
- package/src/core/cron.py +57 -6
- package/src/core/memory.py +3 -90
- package/src/core/metadata.py +75 -0
- package/src/core/skills.py +463 -0
- package/src/core/sub_agent.py +93 -43
- package/src/core/tool_feedback.py +87 -76
- package/src/llm/client.py +7 -2
- package/src/llm/codex_provider.py +350 -0
- package/src/llm/config.py +74 -4
- package/src/llm/prompts.py +261 -502
- package/src/llm/providers.py +28 -7
- package/src/llm/token_tracker.py +32 -1
- package/src/tools/__init__.py +24 -85
- package/src/tools/create_file.py +1 -1
- package/src/tools/directory.py +1 -1
- package/src/tools/edit.py +13 -7
- package/src/tools/file_reader.py +1 -1
- package/src/tools/helpers/__init__.py +1 -7
- package/src/tools/helpers/base.py +65 -16
- package/src/tools/helpers/loader.py +2 -88
- package/src/tools/helpers/path_resolver.py +70 -13
- package/src/tools/helpers/plugin_manifest.py +99 -70
- package/src/tools/review_sub_agent.py +2 -1
- package/src/tools/rg_search.py +119 -35
- package/src/tools/search_plugins.py +140 -72
- package/src/tools/shell.py +3 -3
- package/src/ui/commands.py +470 -33
- package/src/ui/displays.py +27 -1
- package/src/ui/main.py +1 -4
- package/src/ui/tool_confirmation.py +16 -5
- package/src/utils/editor.py +88 -39
- package/src/utils/settings.py +25 -4
- package/src/utils/user_message_logger.py +120 -0
- package/src/utils/validation.py +10 -0
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
|
|
|
@@ -309,22 +344,35 @@ def run_single_job(job: CronJob, console=None, interactive=False) -> None:
|
|
|
309
344
|
from core.chat_manager import ChatManager
|
|
310
345
|
from core.agentic import AgenticOrchestrator
|
|
311
346
|
from utils.paths import RG_EXE_PATH
|
|
312
|
-
from tools.loader import load_all_tools
|
|
313
347
|
from llm.config import TOOLS_ENABLED
|
|
314
348
|
|
|
315
349
|
if not TOOLS_ENABLED:
|
|
316
350
|
raise RuntimeError("Cron requires tools to be enabled")
|
|
317
351
|
|
|
318
|
-
# Ensure tools are loaded
|
|
319
|
-
load_all_tools()
|
|
320
|
-
|
|
321
352
|
# Fresh ChatManager for this job
|
|
322
353
|
chat_manager = ChatManager()
|
|
323
354
|
|
|
324
|
-
#
|
|
355
|
+
# Dream job: auto-approve edits and run cleanup before agent starts
|
|
356
|
+
if job.id == DREAM_JOB_ID:
|
|
357
|
+
chat_manager.approve_mode = "accept_edits"
|
|
358
|
+
from utils.user_message_logger import UserMessageLogger
|
|
359
|
+
removed = UserMessageLogger.cleanup_old_files()
|
|
360
|
+
if removed:
|
|
361
|
+
logger.info("Dream job: removed %d old JSONL files", removed)
|
|
362
|
+
|
|
363
|
+
# Build the prompt — load dream.md for dream job, else use command field
|
|
364
|
+
if job.id == DREAM_JOB_ID:
|
|
365
|
+
dream_prompt_path = Path(__file__).resolve().parents[2] / "prompts" / "main" / "dream.md"
|
|
366
|
+
if dream_prompt_path.is_file():
|
|
367
|
+
command_text = dream_prompt_path.read_text(encoding="utf-8").strip()
|
|
368
|
+
else:
|
|
369
|
+
command_text = job.command
|
|
370
|
+
else:
|
|
371
|
+
command_text = job.command
|
|
372
|
+
|
|
325
373
|
prompt = (
|
|
326
374
|
f"[Cron job: {job.id}]\n"
|
|
327
|
-
f"{
|
|
375
|
+
f"{command_text}"
|
|
328
376
|
)
|
|
329
377
|
|
|
330
378
|
repo_root = Path.cwd().resolve()
|
|
@@ -372,6 +420,9 @@ class CronScheduler:
|
|
|
372
420
|
self._lock = threading.Lock()
|
|
373
421
|
self._running = False
|
|
374
422
|
|
|
423
|
+
# Auto-seed the dream memory job if it doesn't exist
|
|
424
|
+
ensure_dream_job(self.config)
|
|
425
|
+
|
|
375
426
|
def start(self):
|
|
376
427
|
"""Start the cron scheduler background thread."""
|
|
377
428
|
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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""AI-powered metadata generation for skills and plugins.
|
|
2
|
+
|
|
3
|
+
Generates description and tags from content when not provided manually.
|
|
4
|
+
Shared between skill frontmatter and plugin registration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_SYSTEM_PROMPT = """\
|
|
14
|
+
You generate concise metadata for code tools and prompts.
|
|
15
|
+
|
|
16
|
+
Given the content below, return a JSON object with exactly two fields:
|
|
17
|
+
- "description": a one-sentence summary (max 120 chars) of what this does
|
|
18
|
+
- "tags": a list of 3-7 lowercase single-word tags for discovery
|
|
19
|
+
|
|
20
|
+
Return ONLY the JSON object, no other text."""
|
|
21
|
+
|
|
22
|
+
_MAX_CONTENT_CHARS = 3000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_metadata(content: str, name: str = "") -> dict:
|
|
26
|
+
"""Generate description and tags from content using the LLM.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
content: The skill prompt or plugin source to describe.
|
|
30
|
+
name: Optional name for context.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dict with 'description' (str) and 'tags' (list[str]).
|
|
34
|
+
Returns defaults on failure.
|
|
35
|
+
"""
|
|
36
|
+
truncated = content[:_MAX_CONTENT_CHARS]
|
|
37
|
+
if len(content) > _MAX_CONTENT_CHARS:
|
|
38
|
+
truncated += "\n..."
|
|
39
|
+
|
|
40
|
+
user_msg = f"Name: {name}\n\n{truncated}" if name else truncated
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from llm.client import LLMClient
|
|
44
|
+
|
|
45
|
+
client = LLMClient()
|
|
46
|
+
response = client.chat_completion(
|
|
47
|
+
messages=[
|
|
48
|
+
{"role": "system", "content": _SYSTEM_PROMPT},
|
|
49
|
+
{"role": "user", "content": user_msg},
|
|
50
|
+
],
|
|
51
|
+
stream=False,
|
|
52
|
+
tools=None,
|
|
53
|
+
)
|
|
54
|
+
raw = response["choices"][0]["message"]["content"].strip()
|
|
55
|
+
# Strip markdown code fences if present
|
|
56
|
+
raw = re.sub(r"^```(?:json)?\s*\n?", "", raw)
|
|
57
|
+
raw = re.sub(r"\n?```\s*$", "", raw)
|
|
58
|
+
parsed = json.loads(raw)
|
|
59
|
+
|
|
60
|
+
description = str(parsed.get("description", ""))[:120]
|
|
61
|
+
tags = [str(t).lower() for t in parsed.get("tags", []) if t]
|
|
62
|
+
|
|
63
|
+
return {"description": description, "tags": tags}
|
|
64
|
+
|
|
65
|
+
except Exception:
|
|
66
|
+
logger.debug("Metadata generation failed for '%s'", name, exc_info=True)
|
|
67
|
+
return _fallback_metadata(content, name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fallback_metadata(content: str, name: str) -> dict:
|
|
71
|
+
"""Simple heuristic metadata when LLM is unavailable."""
|
|
72
|
+
# Use first line or first ~100 chars as description
|
|
73
|
+
first_line = content.strip().split("\n", 1)[0].strip()
|
|
74
|
+
desc = first_line[:120] if first_line else name
|
|
75
|
+
return {"description": desc, "tags": [name] if name else []}
|