1313class ContextBuilder :
1414 """
1515 Builds the context (system prompt + messages) for the agent.
16-
16+
1717 Assembles bootstrap files, memory, skills, and conversation history
1818 into a coherent prompt for the LLM.
1919 """
20-
20+
2121 BOOTSTRAP_FILES = ["AGENTS.md" , "SOUL.md" , "USER.md" , "TOOLS.md" , "IDENTITY.md" ]
22-
22+
2323 def __init__ (self , workspace : Path ):
2424 self .workspace = workspace
2525 self .memory = MemoryStore (workspace )
2626 self .skills = SkillsLoader (workspace )
27-
27+
2828 def build_system_prompt (self , skill_names : list [str ] | None = None ) -> str :
2929 """
3030 Build the system prompt from bootstrap files, memory, and skills.
31-
31+
3232 Args:
3333 skill_names: Optional list of skills to include.
34-
34+
3535 Returns:
3636 Complete system prompt.
3737 """
3838 parts = []
39-
39+
4040 # Core identity
4141 parts .append (self ._get_identity ())
42-
42+
4343 # Bootstrap files
4444 bootstrap = self ._load_bootstrap_files ()
4545 if bootstrap :
4646 parts .append (bootstrap )
47-
47+
4848 # Memory context
4949 memory = self .memory .get_memory_context ()
5050 if memory :
5151 parts .append (f"# Memory\n \n { memory } " )
52-
52+
5353 # Skills - progressive loading
5454 # 1. Always-loaded skills: include full content
5555 always_skills = self .skills .get_always_skills ()
5656 if always_skills :
5757 always_content = self .skills .load_skills_for_context (always_skills )
5858 if always_content :
5959 parts .append (f"# Active Skills\n \n { always_content } " )
60-
60+
6161 # 2. Available skills: only show summary (agent uses read_file to load)
6262 skills_summary = self .skills .build_skills_summary ()
6363 if skills_summary :
@@ -67,17 +67,18 @@ def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
6767Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.
6868
6969{ skills_summary } """ )
70-
70+
7171 return "\n \n ---\n \n " .join (parts )
72-
72+
7373 def _get_identity (self ) -> str :
7474 """Get the core identity section."""
7575 from datetime import datetime
76+
7677 now = datetime .now ().strftime ("%Y-%m-%d %H:%M (%A)" )
7778 workspace_path = str (self .workspace .expanduser ().resolve ())
7879 system = platform .system ()
7980 runtime = f"{ 'macOS' if system == 'Darwin' else system } { platform .machine ()} , Python { platform .python_version ()} "
80-
81+
8182 return f"""# nanobot 🐈
8283
8384You are nanobot, a helpful AI assistant. You have access to tools that allow you to:
@@ -105,19 +106,19 @@ def _get_identity(self) -> str:
105106
106107Always be helpful, accurate, and concise. When using tools, explain what you're doing.
107108When remembering something, write to { workspace_path } /memory/MEMORY.md"""
108-
109+
109110 def _load_bootstrap_files (self ) -> str :
110111 """Load all bootstrap files from workspace."""
111112 parts = []
112-
113+
113114 for filename in self .BOOTSTRAP_FILES :
114115 file_path = self .workspace / filename
115116 if file_path .exists ():
116117 content = file_path .read_text (encoding = "utf-8" )
117118 parts .append (f"## { filename } \n \n { content } " )
118-
119+
119120 return "\n \n " .join (parts ) if parts else ""
120-
121+
121122 def build_messages (
122123 self ,
123124 history : list [dict [str , Any ]],
@@ -162,7 +163,7 @@ def _build_user_content(self, text: str, media: list[str] | None) -> str | list[
162163 """Build user message content with optional base64-encoded images."""
163164 if not media :
164165 return text
165-
166+
166167 images = []
167168 for path in media :
168169 p = Path (path )
@@ -171,38 +172,31 @@ def _build_user_content(self, text: str, media: list[str] | None) -> str | list[
171172 continue
172173 b64 = base64 .b64encode (p .read_bytes ()).decode ()
173174 images .append ({"type" : "image_url" , "image_url" : {"url" : f"data:{ mime } ;base64,{ b64 } " }})
174-
175+
175176 if not images :
176177 return text
177178 return images + [{"type" : "text" , "text" : text }]
178-
179+
179180 def add_tool_result (
180- self ,
181- messages : list [dict [str , Any ]],
182- tool_call_id : str ,
183- tool_name : str ,
184- result : str
181+ self , messages : list [dict [str , Any ]], tool_call_id : str , tool_name : str , result : str
185182 ) -> list [dict [str , Any ]]:
186183 """
187184 Add a tool result to the message list.
188-
185+
189186 Args:
190187 messages: Current message list.
191188 tool_call_id: ID of the tool call.
192189 tool_name: Name of the tool.
193190 result: Tool execution result.
194-
191+
195192 Returns:
196193 Updated message list.
197194 """
198- messages .append ({
199- "role" : "tool" ,
200- "tool_call_id" : tool_call_id ,
201- "name" : tool_name ,
202- "content" : result
203- })
195+ messages .append (
196+ {"role" : "tool" , "tool_call_id" : tool_call_id , "name" : tool_name , "content" : result }
197+ )
204198 return messages
205-
199+
206200 def add_assistant_message (
207201 self ,
208202 messages : list [dict [str , Any ]],
@@ -212,24 +206,24 @@ def add_assistant_message(
212206 ) -> list [dict [str , Any ]]:
213207 """
214208 Add an assistant message to the message list.
215-
209+
216210 Args:
217211 messages: Current message list.
218212 content: Message content.
219213 tool_calls: Optional tool calls.
220214 reasoning_content: Thinking output (Kimi, DeepSeek-R1, etc.).
221-
215+
222216 Returns:
223217 Updated message list.
224218 """
225219 msg : dict [str , Any ] = {"role" : "assistant" , "content" : content or "" }
226-
220+
227221 if tool_calls :
228222 msg ["tool_calls" ] = tool_calls
229-
223+
230224 # Thinking models reject history without this
231225 if reasoning_content :
232226 msg ["reasoning_content" ] = reasoning_content
233-
227+
234228 messages .append (msg )
235229 return messages
0 commit comments