Build a rich system prompt incorporating agent's full context. Reads from workspace files and DB: - soul.md → personality - memory.md → long-term memory - skills/ → skill names + summaries - Database → relationship network (human + agent)
(agent_id: uuid.UUID, agent_name: str, role_description: str = "", current_user_name: str = None)
| 241 | |
| 242 | |
| 243 | async def build_agent_context(agent_id: uuid.UUID, agent_name: str, role_description: str = "", current_user_name: str = None) -> tuple[str, str]: |
| 244 | """Build a rich system prompt incorporating agent's full context. |
| 245 | |
| 246 | Reads from workspace files and DB: |
| 247 | - soul.md → personality |
| 248 | - memory.md → long-term memory |
| 249 | - skills/ → skill names + summaries |
| 250 | - Database → relationship network (human + agent) |
| 251 | """ |
| 252 | # --- Soul --- |
| 253 | # Soul is the agent's full author-curated identity; detailed souls (e.g. |
| 254 | # bundle agents) run 4-12k chars. A tight cap silently drops every tail |
| 255 | # section — rules, boundaries, facts — and the agent then confidently |
| 256 | # denies things its soul plainly states, with no log of the truncation. |
| 257 | # Memory and relationships below keep small caps because they grow |
| 258 | # unbounded at runtime; the soul does not (only seeded/explicitly edited). |
| 259 | soul = await _read_file_safe(normalize_storage_key(f"{agent_id}/soul.md"), 30000) |
| 260 | # Strip markdown heading if present |
| 261 | if soul.startswith("# "): |
| 262 | soul = "\n".join(soul.split("\n")[1:]).strip() |
| 263 | |
| 264 | # --- Memory --- |
| 265 | memory = await _read_file_safe(normalize_storage_key(f"{agent_id}/memory/memory.md"), 2000) |
| 266 | if not memory: |
| 267 | memory = await _read_file_safe(normalize_storage_key(f"{agent_id}/memory.md"), 2000) |
| 268 | if memory.startswith("# "): |
| 269 | memory = "\n".join(memory.split("\n")[1:]).strip() |
| 270 | |
| 271 | # --- Skills index (progressive disclosure) --- |
| 272 | skills_text = await _load_skills_index(agent_id) |
| 273 | |
| 274 | # --- Relationships --- |
| 275 | from app.database import async_session |
| 276 | async with async_session() as db: |
| 277 | relationships = await _load_relationships_from_db(db, agent_id) |
| 278 | |
| 279 | # --- Compose static and dynamic system prompt blocks --- |
| 280 | from datetime import datetime, timezone as _tz |
| 281 | from app.services.timezone_utils import get_agent_timezone, now_in_timezone |
| 282 | agent_tz_name = await get_agent_timezone(agent_id) |
| 283 | agent_local_now = now_in_timezone(agent_tz_name) |
| 284 | now_str = agent_local_now.strftime(f"%Y-%m-%d %H:%M:%S ({agent_tz_name})") |
| 285 | |
| 286 | static_parts = [f"You are {agent_name}, an enterprise digital employee."] |
| 287 | |
| 288 | |
| 289 | if role_description: |
| 290 | static_parts.append(f"\n## Role\n{role_description}") |
| 291 | |
| 292 | if agent_name == "OKR Agent": |
| 293 | static_parts.append(""" |
| 294 | ## Daily Report Recording Rules |
| 295 | |
| 296 | 🔴 **ABSOLUTE RULE — MUST CALL `upsert_member_daily_report` IMMEDIATELY:** |
| 297 | When ANY tracked member or agent sends you content that looks like a daily work update, status report, or progress note — **IMMEDIATELY call `upsert_member_daily_report` in the SAME response turn. Do NOT:** |
| 298 | - First explain what you plan to do, then call the tool in a second turn |
| 299 | - Claim the tool is unavailable, broken, or unknown — **it is ALWAYS available** |
| 300 | - Write the report to memory, Focus, or any file instead |