diff --git a/navi/core/agent.py b/navi/core/agent.py index bd26560..70f32a9 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -95,15 +95,6 @@ tool_schemas = [t.schema() for t in tools] llm = self._get_backend(profile.llm_backend) - # System prompt only goes into context (not display history). - # Use role check rather than empty check: backward-compat sessions have - # context initialised from messages (which never contain a system message). - if not any(m.role == "system" for m in session.context): - session.context.insert(0, Message( - role="system", - content=self._build_system_prompt(profile.system_prompt), - )) - mem = await self._memory_msg() # Expose session_id to tools (e.g. SSH connection pool) via ContextVar @@ -119,7 +110,7 @@ for iteration in range(profile.max_iterations): log.debug("agent.iteration", session_id=session_id, iteration=iteration) response = await llm.complete( - self._with_memory(session.context, mem), + self._build_context(session.context, profile, mem), tools=tool_schemas if tools else None, temperature=profile.temperature, model=profile.model, @@ -175,11 +166,10 @@ mem = await self._memory_msg() + # Sub-agent context: only user/assistant/tool messages — system is injected dynamically. context: list[Message] = [ - Message(role="system", content=self._build_system_prompt(profile.system_prompt)) + Message(role="user", content=user_message, created_at=datetime.now(timezone.utc)) ] - context.append(Message(role="user", content=user_message, - created_at=datetime.now(timezone.utc))) # Read the event sink set by the parent run_stream() for this tool call. # If None (e.g. called from run(), not run_stream()), events are silently dropped. @@ -197,7 +187,7 @@ turn_tool_calls: list[ToolCallRequest] | None = None async for chunk in llm.stream_complete( - self._with_memory(context, mem), + self._build_context(context, profile, mem), tools=tool_schemas if tools else None, temperature=profile.temperature, model=profile.model, @@ -284,15 +274,6 @@ tool_schemas = [t.schema() for t in tools] llm = self._get_backend(profile.llm_backend) - # System prompt only goes into context (not display history). - # Use role check rather than empty check: backward-compat sessions have - # context initialised from messages (which never contain a system message). - if not any(m.role == "system" for m in session.context): - session.context.insert(0, Message( - role="system", - content=self._build_system_prompt(profile.system_prompt), - )) - mem = await self._memory_msg() # Expose session_id to tools (e.g. SSH connection pool) via ContextVar @@ -350,7 +331,7 @@ context_tokens: int | None = None async for chunk in llm.stream_complete( - self._with_memory(session.context, mem), + self._build_context(session.context, profile, mem), tools=tool_schemas if tools else None, temperature=profile.temperature, model=profile.model, @@ -490,13 +471,30 @@ return None return Message(role="system", content=f"## What I remember about the user\n\n{summary}") - def _with_memory(self, ctx: list[Message], mem: "Message | None") -> list[Message]: - """Inject memory message after the first system message without mutating ctx.""" - if mem is None: - return ctx - if ctx and ctx[0].role == "system": - return [ctx[0], mem] + ctx[1:] - return [mem] + ctx + def _build_context( + self, + session_context: list[Message], + profile: "AgentProfile", + mem: "Message | None", + ) -> list[Message]: + """Build the full LLM context for one call. + + System prompt is injected fresh from the current profile every time — + it is NOT stored in session.context so that profile switches take + effect immediately without touching stored history. + Memory (if any) is placed right after the system message. + Any system messages already in session.context are stripped (migration safety). + """ + system_msg = Message( + role="system", + content=self._build_system_prompt(profile.system_prompt), + ) + conv = [m for m in session_context if m.role != "system"] + result: list[Message] = [system_msg] + if mem: + result.append(mem) + result.extend(conv) + return result def _build_system_prompt(self, profile_prompt: str) -> str: persona = settings.navi_persona.strip()