bone-agent 1.3.3 → 2.0.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.
Files changed (121) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -184
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -141
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -36
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/targeted_searching.md +0 -10
  25. package/prompts/main/task_lists_pattern.md +0 -8
  26. package/prompts/main/temp_folder.md +0 -9
  27. package/prompts/main/think_before_acting.md +0 -10
  28. package/prompts/main/tone_and_style.md +0 -4
  29. package/prompts/main/tool_preferences.md +0 -24
  30. package/prompts/main/trust_subagent_context.md +0 -21
  31. package/prompts/main/when_to_use_sub_agent.md +0 -7
  32. package/prompts/micro/ask_questions.md +0 -1
  33. package/prompts/micro/batch_independent_calls.md +0 -1
  34. package/prompts/micro/casual_interactions.md +0 -1
  35. package/prompts/micro/code_references.md +0 -1
  36. package/prompts/micro/communication_style.md +0 -1
  37. package/prompts/micro/context_reliability.md +0 -1
  38. package/prompts/micro/conversational_tool_calling.md +0 -1
  39. package/prompts/micro/editing_pattern.md +0 -1
  40. package/prompts/micro/error_handling.md +0 -1
  41. package/prompts/micro/exploration_pattern.md +0 -1
  42. package/prompts/micro/intro.md +0 -1
  43. package/prompts/micro/obsidian.md +0 -4
  44. package/prompts/micro/obsidian_project.md +0 -5
  45. package/prompts/micro/professional_objectivity.md +0 -1
  46. package/prompts/micro/targeted_searching.md +0 -1
  47. package/prompts/micro/task_lists_pattern.md +0 -1
  48. package/prompts/micro/temp_folder.md +0 -1
  49. package/prompts/micro/think_before_acting.md +0 -5
  50. package/prompts/micro/tone_and_style.md +0 -1
  51. package/prompts/micro/tool_preferences.md +0 -1
  52. package/prompts/micro/trust_subagent_context.md +0 -1
  53. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  54. package/requirements.txt +0 -9
  55. package/src/__init__.py +0 -11
  56. package/src/core/__init__.py +0 -1
  57. package/src/core/agentic.py +0 -985
  58. package/src/core/chat_manager.py +0 -1564
  59. package/src/core/config_manager.py +0 -253
  60. package/src/core/cron.py +0 -582
  61. package/src/core/cron_allowlist.py +0 -118
  62. package/src/core/memory.py +0 -145
  63. package/src/core/retry.py +0 -71
  64. package/src/core/sub_agent.py +0 -326
  65. package/src/core/tool_approval.py +0 -220
  66. package/src/core/tool_feedback.py +0 -778
  67. package/src/exceptions.py +0 -79
  68. package/src/llm/__init__.py +0 -1
  69. package/src/llm/client.py +0 -171
  70. package/src/llm/config.py +0 -492
  71. package/src/llm/prompts.py +0 -489
  72. package/src/llm/providers.py +0 -436
  73. package/src/llm/streaming.py +0 -163
  74. package/src/llm/token_tracker.py +0 -384
  75. package/src/tools/__init__.py +0 -212
  76. package/src/tools/constants.py +0 -59
  77. package/src/tools/create_file.py +0 -136
  78. package/src/tools/directory.py +0 -389
  79. package/src/tools/edit.py +0 -545
  80. package/src/tools/file_reader.py +0 -322
  81. package/src/tools/helpers/__init__.py +0 -105
  82. package/src/tools/helpers/base.py +0 -550
  83. package/src/tools/helpers/converters.py +0 -44
  84. package/src/tools/helpers/file_helpers.py +0 -189
  85. package/src/tools/helpers/formatters.py +0 -411
  86. package/src/tools/helpers/loader.py +0 -231
  87. package/src/tools/helpers/parallel_executor.py +0 -231
  88. package/src/tools/helpers/path_resolver.py +0 -232
  89. package/src/tools/helpers/plugin_manifest.py +0 -156
  90. package/src/tools/obsidian.py +0 -96
  91. package/src/tools/review_sub_agent.py +0 -189
  92. package/src/tools/rg_search.py +0 -460
  93. package/src/tools/search_plugins.py +0 -109
  94. package/src/tools/select_option.py +0 -600
  95. package/src/tools/shell.py +0 -302
  96. package/src/tools/sub_agent.py +0 -139
  97. package/src/tools/task_list.py +0 -269
  98. package/src/tools/web_search.py +0 -61
  99. package/src/ui/__init__.py +0 -1
  100. package/src/ui/banner.py +0 -87
  101. package/src/ui/commands.py +0 -2809
  102. package/src/ui/displays.py +0 -214
  103. package/src/ui/loader.py +0 -284
  104. package/src/ui/main.py +0 -647
  105. package/src/ui/prompt_utils.py +0 -113
  106. package/src/ui/setting_selector.py +0 -590
  107. package/src/ui/setup_wizard.py +0 -294
  108. package/src/ui/sub_agent_panel.py +0 -234
  109. package/src/ui/tool_confirmation.py +0 -215
  110. package/src/utils/__init__.py +0 -1
  111. package/src/utils/citation_parser.py +0 -199
  112. package/src/utils/editor.py +0 -158
  113. package/src/utils/gitignore_filter.py +0 -149
  114. package/src/utils/logger.py +0 -254
  115. package/src/utils/paths.py +0 -30
  116. package/src/utils/result_parsers.py +0 -108
  117. package/src/utils/safe_commands.py +0 -243
  118. package/src/utils/settings.py +0 -191
  119. package/src/utils/user_message_logger.py +0 -120
  120. package/src/utils/validation.py +0 -191
  121. package/src/utils/web_search.py +0 -173
@@ -1,489 +0,0 @@
1
-
2
-
3
- # Modular prompt composition for native function calling
4
- #
5
- # All prompt sections are loaded from file-based variants under prompts/<variant>/.
6
- # Each variant directory contains individual .md files for each section.
7
- # Section ordering is defined programmatically in _main_sections() and
8
- # _sub_agent_sections() — no manifest files needed.
9
-
10
- import logging
11
-
12
- logger = logging.getLogger(__name__)
13
- from pathlib import Path
14
- from string import Template
15
-
16
- # Root of the prompts directory (repo root / prompts)
17
- _PROMPTS_DIR = Path(__file__).resolve().parents[2] / "prompts"
18
-
19
-
20
- # Mode section for main agent
21
-
22
- MODE_SECTION = """## Current mode: Edit
23
-
24
- **Important:** Explain changes conceptually, show code only in edit tools
25
-
26
- Workflow:
27
- 1. Analyze request and identify files to modify
28
- 2. Generate a brief plan (what/where/why, no code)
29
- 3. **Check for trade-offs** - If multiple valid approaches exist, use select_option to clarify
30
- 4. Proceed with edits
31
-
32
- When the user asks for a plan (e.g. "plan this out", "what's involved", "before you start"):
33
- - Explore and understand requirements first
34
- - Propose a structured plan with bullet points: what changes, where, and why
35
- - Highlight trade-offs and ambiguities using select_option
36
- - End with a summary of the proposed changes
37
- - Ask: 'Do you approve this plan?' before proceeding with edits
38
-
39
- Show code only when using `edit_file`/`create_file` tools. Keep text explanations concise."""
40
-
41
-
42
- # Sub-agent specific sections (research-focused, read-only tools passed via function calling)
43
-
44
- SUB_AGENT_SECTIONS = {
45
- "token_budget": """## Token Budget
46
-
47
- You have a total budget of approximately $hard_limit tokens for this task. When you reach $soft_limit tokens, you MUST immediately stop exploring and return your findings to the main agent. Do not continue reading files, searching, or making tool calls once you are near or past the soft limit. Wrap up your answer with citations and return it promptly.""",
48
-
49
- "response_format": """# Response Format
50
-
51
- When answering the main agent's query:
52
-
53
- 1. **Provide a clear summary** of your findings
54
- 2. **Cite only the most relevant files with precise line ranges** for code you've actually read
55
-
56
- **Important:** Only cite files where you have actually read the content. The main agent will
57
- inject the actual file contents based on your citations and will trust these injected contents
58
- without re-reading them.
59
-
60
- **Required:** You must use bracketed citation formats only. Unbracketed formats like `file:N`
61
- will not be recognized and will be ignored.
62
-
63
- Use these citation formats:
64
- - `[path/to/file] (lines N-M)` - for a specific range you've fully read (preferred)
65
- - `[path/to/file]:N-M` - bracketed range notation (preferred)
66
- - `[path/to/file]:N` - bracketed single line notation (preferred)
67
- - `[path/to/file] (full)` - only for small files or when you genuinely need the entire file
68
-
69
- **Citation Guidelines - Be Selective:**
70
- - Be precise with line numbers - cite only the specific ranges that matter
71
- - Prioritize specific ranges (lines N-M) over full files
72
- - Avoid citing large files with (full) - use specific ranges instead
73
- - Omit boilerplate, tests, and utility code unless directly relevant
74
- - The main agent can always request more context if needed
75
-
76
- Example:
77
- "The authentication flow starts in [src/core/auth.py] (lines 45-78) where tokens are validated,
78
- then calls [src/core/session.py] (lines 112-145) for session management."
79
-
80
- The main agent will automatically inject the actual file contents based on your citations,
81
- so the main agent doesn't need to re-read files you've already explored.""",
82
-
83
- "mode": """# Current mode: Research
84
-
85
- You are a research sub-agent. Answer the specific question asked — do not explore the whole subsystem. Use read-only tools (rg, read_file, list_directory) to gather just enough information.
86
-
87
- **Stop early:** Answer when you can address the query. The main agent can call you again for follow-up if needed. Prefer the most likely paths based on codebase structure.""",
88
-
89
- "review_mode": """# Current mode: Code Review
90
-
91
- You are a code review agent. Analyze the provided git diff and provide honest, useful feedback.
92
- Your output goes directly to the user — write clean, readable markdown.
93
-
94
- ## Workflow
95
- 1. Parse file paths from diff headers (`+++ b/` or `--- a/`)
96
- 2. Use `read_file` on each changed file for surrounding context
97
- 3. Cross-reference related files when needed
98
- 4. Write your review
99
-
100
- ## Output Template
101
-
102
- Follow this exact structure. Do not add extra sections or reorder.
103
-
104
- ### Summary
105
- One paragraph (2-4 sentences). What changed, overall quality. If nothing noteworthy, say so.
106
-
107
- ### Issues
108
- Group issues by severity under sub-headings. Only include levels that have findings.
109
-
110
- #### Critical (N)
111
- - `[path/to/file]:line` — short description
112
-
113
- #### Warning (N)
114
- - `[path/to/file]:line` — short description
115
-
116
- #### Info (N)
117
- - `[path/to/file]:line` — short description
118
-
119
- Severity levels:
120
- - **critical** — Blocking. Must fix before merge. Use sparingly.
121
- - **warning** — Should fix, not blocking.
122
- - **info** — Style, naming, nitpicks.
123
-
124
- One bullet per issue. One line each. No paragraphs. Keep descriptions brief.
125
-
126
- ### Verdict
127
- Always end with a verdict. One line: `APPROVE - explanation` or `REQUEST CHANGES - explanation`.
128
- - `APPROVE` — no critical issues. Mention what looked good or minor nits.
129
- - `REQUEST CHANGES` — critical issues found. Summarize what needs fixing.
130
-
131
- ## Anti-Fabrication Rule
132
- Do not manufacture issues or inflate severity. If nothing is wrong, say so in the summary and skip those labels. An honest "No issues found" beats a fabricated nitpick. Use bracketed citations: `[path/to/file]:line_number`.""",
133
- }
134
-
135
-
136
- # Builder functions to compose prompts from sections
137
-
138
- # Mapping of prompt section keys to the tool names they depend on.
139
- # If ALL listed tools are disabled, the section is omitted from the prompt.
140
- # Sections not listed have no tool dependency and are always included.
141
- SECTION_TOOL_DEPS = {
142
- "trust_subagent_context": ["sub_agent"],
143
- "when_to_use_sub_agent": ["sub_agent"],
144
- "ask_questions": ["select_option"],
145
- "editing_pattern": ["edit_file"],
146
- "task_lists_pattern": ["create_task_list", "complete_task", "show_task_list", "edit_file"],
147
- "temp_folder": ["create_file"],
148
- }
149
-
150
-
151
- def _build_memory_section() -> str | None:
152
- """Build the read-only memory context section for the system prompt.
153
-
154
- Injects live memory content blocks with capacity headers.
155
- No writing instructions — memory files are read-only during conversations.
156
- All writes happen through the dream cron job.
157
-
158
- Returns None if MemoryManager is not initialized or memory is disabled.
159
- """
160
- from llm.config import MEMORY_SETTINGS
161
- if not MEMORY_SETTINGS.get("enabled", True):
162
- return None
163
-
164
- try:
165
- from core.memory import MemoryManager
166
- manager = MemoryManager.get_instance()
167
- if manager is None:
168
- return None
169
-
170
- result = ""
171
-
172
- # Append capacity headers and memory content if files have real content
173
- user_content = manager.load_user_memory()
174
- user_usage = manager.get_user_usage()
175
- if manager._has_entries(user_content):
176
- pct = user_usage["chars_used"] * 100 // user_usage["chars_limit"]
177
- result += f"USER MEMORY [{pct}% — {user_usage['chars_used']}/{user_usage['chars_limit']} chars]\n{user_content.strip()}\n\n"
178
-
179
- project_content = manager.load_project_memory()
180
- project_usage = manager.get_project_usage()
181
- if manager._has_entries(project_content):
182
- pct = project_usage["chars_used"] * 100 // project_usage["chars_limit"]
183
- result += f"PROJECT MEMORY [{pct}% — {project_usage['chars_used']}/{project_usage['chars_limit']} chars]\n{project_content.strip()}\n\n"
184
-
185
- return result.strip() if result else None
186
- except Exception:
187
- return None
188
-
189
-
190
- def _build_vault_section(variant: str = "main") -> str | None:
191
- """Build the Obsidian vault section for the system prompt.
192
-
193
- Loads obsidian.md from prompts/<variant>/ and substitutes dynamic values
194
- (vault root, project folder, excluded folders) using string.Template.
195
- If project exists, also loads and appends obsidian_project.md from the
196
- same variant directory.
197
-
198
- Returns None if vault is not active.
199
- """
200
- try:
201
- from utils.settings import obsidian_settings
202
- if not obsidian_settings.is_active():
203
- return None
204
- except Exception as e:
205
- logger.debug("Obsidian not available: %s", e)
206
- return None
207
-
208
- try:
209
- from tools.obsidian import get_vault_session, init_session
210
- session = get_vault_session()
211
- # Initialize session on first prompt build if not yet available.
212
- # Normally initialized by AgenticLoop.__init__, but the system prompt
213
- # is built earlier (in ChatManager.__init__), causing an inconsistent
214
- # vault section (missing note schemas) on fresh start.
215
- if session is None:
216
- session = init_session()
217
- except Exception:
218
- session = None
219
-
220
- vault_root = str(session.vault_root) if session else "<not available>"
221
- project_folder = str(session.project_folder) if session else "<not available>"
222
-
223
- project_exists = (
224
- session
225
- and session.project_folder.is_dir()
226
- and (session.project_folder / "Bugs").is_dir()
227
- )
228
-
229
- excluded = obsidian_settings.exclude_folders
230
-
231
- # Load base obsidian template from variant directory
232
- base_path = _PROMPTS_DIR / variant / "obsidian.md"
233
- if not base_path.is_file():
234
- logger.warning(
235
- "Obsidian template not found at %s — vault section omitted", base_path
236
- )
237
- return None
238
-
239
- base_content = base_path.read_text(encoding="utf-8").strip()
240
-
241
- # Substitute dynamic values using string.Template
242
- if project_exists:
243
- project_header = f"**Project folder:** `{project_folder}`"
244
- else:
245
- project_header = "**Project:** not initialized (run `/obsidian init` to create)"
246
-
247
- formatted = Template(base_content).safe_substitute(
248
- vault_root=vault_root,
249
- project_folder=project_folder,
250
- project_header=project_header,
251
- excluded=excluded,
252
- )
253
-
254
- # Append project-specific section if project exists
255
- if project_exists:
256
- project_path = _PROMPTS_DIR / variant / "obsidian_project.md"
257
- if project_path.is_file():
258
- project_content = project_path.read_text(encoding="utf-8").strip()
259
- formatted = formatted + "\n\n" + project_content
260
-
261
- return formatted
262
-
263
-
264
- def _build_context_section() -> str:
265
- """Build a dynamic section with current date, time, and location."""
266
- from datetime import datetime
267
-
268
- now = datetime.now()
269
- date_str = now.strftime("%A, %B %d, %Y")
270
- time_str = now.strftime("%I:%M %p")
271
- timezone = now.astimezone().tzinfo
272
-
273
- return (
274
- "## Current Context\n\n"
275
- f"**Date:** {date_str}\n"
276
- f"**Time:** {time_str} ({timezone})\n"
277
- )
278
-
279
-
280
- def _static(variant: str, name: str) -> str:
281
- """Load a static .md section from prompts/<variant>/."""
282
- path = _PROMPTS_DIR / variant / name
283
- if path.is_file():
284
- return path.read_text(encoding="utf-8").strip()
285
- logger.warning("Section file not found: %s — prompt section '%s' omitted", path, name)
286
- return ""
287
-
288
-
289
- def _resolve_variant() -> str:
290
- """Resolve the prompt variant from settings, falling back to 'main'."""
291
- try:
292
- from utils.settings import prompt_settings
293
- return prompt_settings.variant
294
- except Exception:
295
- return "main"
296
-
297
-
298
- def _should_include_section(section_key: str) -> bool:
299
- """Check whether a prompt section should be included based on tool availability.
300
-
301
- A section is skipped only when ALL of its dependent tools are disabled.
302
- Uses lazy import to avoid circular dependency with tools module.
303
- """
304
- deps = SECTION_TOOL_DEPS.get(section_key)
305
- if not deps:
306
- return True
307
- from tools.helpers.base import ToolRegistry
308
- return not all(ToolRegistry.is_disabled(t) for t in deps)
309
-
310
-
311
- def _build_prompt_to_list(sections: list[tuple[str, callable]]) -> list[str]:
312
- """Build prompt as a list of section strings from (key, content_fn) pairs.
313
-
314
- Skips sections that fail _should_include_section (tool deps)
315
- or whose content_fn returns None/empty.
316
- """
317
- result = []
318
- for key, content_fn in sections:
319
- if not _should_include_section(key):
320
- continue
321
- content = content_fn()
322
- if content:
323
- result.append(content)
324
- return result
325
-
326
-
327
- def _build_prompt(sections: list[tuple[str, callable]]) -> str:
328
- """Build prompt string from (key, content_fn) pairs.
329
-
330
- Delegates to _build_prompt_to_list and joins the result.
331
- """
332
- return "\n\n".join(_build_prompt_to_list(sections))
333
-
334
-
335
- def _variant_available(variant: str) -> bool:
336
- """Check if a variant directory exists.
337
-
338
- Raises OSError if directory permissions prevent access.
339
- """
340
- return (_PROMPTS_DIR / variant).is_dir()
341
-
342
-
343
- def _list_variants() -> list[str]:
344
- """List available prompt variants (directories under prompts/)."""
345
- if not _PROMPTS_DIR.is_dir():
346
- return []
347
- return sorted(
348
- d.name for d in _PROMPTS_DIR.iterdir()
349
- if d.is_dir()
350
- )
351
-
352
-
353
- def _main_sections(variant: str) -> list[tuple[str, callable]]:
354
- """Return (key, content_fn) pairs for the main agent prompt.
355
-
356
- Order in this list = order in the final prompt.
357
- Static .md sections, dynamic builders, and hardcoded sections all
358
- live here — single source of truth for ordering.
359
- """
360
- return [
361
- ("intro", lambda: _static(variant, "intro.md")),
362
- ("context", _build_context_section),
363
- ("tone_and_style", lambda: _static(variant, "tone_and_style.md")),
364
- ("communication_style", lambda: _static(variant, "communication_style.md")),
365
- ("trust_subagent_context", lambda: _static(variant, "trust_subagent_context.md")),
366
- ("context_reliability", lambda: _static(variant, "context_reliability.md")),
367
- ("conversational_tool_calling", lambda: _static(variant, "conversational_tool_calling.md")),
368
- ("professional_objectivity", lambda: _static(variant, "professional_objectivity.md")),
369
- ("think_before_acting", lambda: _static(variant, "think_before_acting.md")),
370
- ("batch_independent_calls", lambda: _static(variant, "batch_independent_calls.md")),
371
- ("code_references", lambda: _static(variant, "code_references.md")),
372
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
373
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
374
- ("editing_pattern", lambda: _static(variant, "editing_pattern.md")),
375
- ("task_lists_pattern", lambda: _static(variant, "task_lists_pattern.md")),
376
- ("casual_interactions", lambda: _static(variant, "casual_interactions.md")),
377
- ("ask_questions", lambda: _static(variant, "ask_questions.md")),
378
- ("tool_preferences", lambda: _static(variant, "tool_preferences.md")),
379
- ("when_to_use_sub_agent", lambda: _static(variant, "when_to_use_sub_agent.md")),
380
- ("error_handling", lambda: _static(variant, "error_handling.md")),
381
- ("temp_folder", lambda: _static(variant, "temp_folder.md")),
382
- ("memory_system", _build_memory_section),
383
- ("obsidian", lambda: _build_vault_section(variant)),
384
- ("mode", lambda: MODE_SECTION),
385
- ]
386
-
387
-
388
- def _sub_agent_sections(variant: str) -> list[tuple[str, callable]]:
389
- """Return (key, content_fn) pairs for the sub-agent prompt.
390
-
391
- The micro variant has a smaller set of sections than main.
392
- response_format is placed explicitly after code_references.
393
- """
394
- # Base sections shared across all variants
395
- base = [
396
- ("intro", lambda: _static(variant, "intro.md")),
397
- ("context", _build_context_section),
398
- ("tone_and_style", lambda: _static(variant, "tone_and_style.md")),
399
- ("communication_style", lambda: _static(variant, "communication_style.md")),
400
- ]
401
-
402
- # Micro variant has a different section set (no conversational_tool_calling,
403
- # no professional_objectivity, no think_before_acting, etc.)
404
- if variant == "micro":
405
- middle = [
406
- ("trust_subagent_context", lambda: _static(variant, "trust_subagent_context.md")),
407
- ("context_reliability", lambda: _static(variant, "context_reliability.md")),
408
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
409
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
410
- ("tool_preferences", lambda: _static(variant, "tool_preferences.md")),
411
- ]
412
- else:
413
- middle = [
414
- ("conversational_tool_calling", lambda: _static(variant, "conversational_tool_calling.md")),
415
- ("professional_objectivity", lambda: _static(variant, "professional_objectivity.md")),
416
- ("think_before_acting", lambda: _static(variant, "think_before_acting.md")),
417
- ("batch_independent_calls", lambda: _static(variant, "batch_independent_calls.md")),
418
- ("code_references", lambda: _static(variant, "code_references.md")),
419
- ("response_format", lambda: SUB_AGENT_SECTIONS["response_format"]),
420
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
421
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
422
- ("casual_interactions", lambda: _static(variant, "casual_interactions.md")),
423
- ("temp_folder", lambda: _static(variant, "temp_folder.md")),
424
- ]
425
-
426
- return base + middle
427
-
428
-
429
- def build_system_prompt(variant: str | None = None) -> str:
430
- """Build system prompt for main agent.
431
-
432
- Loads section content from prompts/<variant>/. Order is defined by
433
- _main_sections(). Raises FileNotFoundError if variant directory is missing.
434
-
435
- Args:
436
- variant: Variant name (e.g. 'main', 'micro').
437
- If None, reads from settings.
438
-
439
- Returns:
440
- Complete system prompt string
441
- """
442
- if variant is None:
443
- variant = _resolve_variant()
444
- if not _variant_available(variant):
445
- raise FileNotFoundError(
446
- f"Prompt variant '{variant}' not found: "
447
- f"{_PROMPTS_DIR / variant} does not exist"
448
- )
449
- return _build_prompt(_main_sections(variant))
450
-
451
-
452
- def build_sub_agent_prompt(sub_agent_type: str = "research", soft_limit_tokens: int | None = None, hard_limit_tokens: int | None = None) -> str:
453
- """Build prompt for sub-agent (research or review, read-only).
454
-
455
- Args:
456
- sub_agent_type: Type of sub-agent ('research' or 'review').
457
- soft_limit_tokens: Soft token limit to display in prompt.
458
- hard_limit_tokens: Hard token limit to display in prompt.
459
-
460
- Returns:
461
- Complete system prompt string
462
- """
463
- variant = _resolve_variant()
464
- if not _variant_available(variant):
465
- raise FileNotFoundError(
466
- f"Sub-agent prompt variant '{variant}' not found: "
467
- f"{_PROMPTS_DIR / variant} does not exist"
468
- )
469
-
470
- result = _build_prompt_to_list(_sub_agent_sections(variant))
471
-
472
- # Append parameterized sections (always last)
473
- if soft_limit_tokens is not None and hard_limit_tokens is not None:
474
- result.append(
475
- Template(SUB_AGENT_SECTIONS["token_budget"]).safe_substitute(
476
- soft_limit=f"{soft_limit_tokens:,}",
477
- hard_limit=f"{hard_limit_tokens:,}",
478
- )
479
- )
480
-
481
- if sub_agent_type == "review":
482
- result.append(SUB_AGENT_SECTIONS["review_mode"])
483
- else:
484
- result.append(SUB_AGENT_SECTIONS["mode"])
485
-
486
- return "\n\n".join(result)
487
-
488
-
489
-