Compare commits

...
Author SHA1 Message Date
aec96f9d5d fix(cli): pass prior conversation turns to agent.run() in chat REPL (#744)
* fix(cli): pass prior conversation turns to agent.run() in chat REPL

Why: jarvis chat built an AgentContext seeded only with an optional
memory-injected fact, never with the turn-by-turn conversation history
already tracked in `history`. Every agent-backed chat turn after the
first ran with no memory of what was said before it.

- src/openjarvis/cli/chat_cmd.py: always build AgentContext for
  agent-backed turns, seeded from prior non-system history messages,
  still layering the memory-fact message on top when present
- tests/cli/test_chat_cmd.py: regression test asserting the second
  turn's AgentContext carries the first turn's user/assistant messages

* fix(cli): keep memory context before chat history

---------

Co-authored-by: Ari <ari.silva@paipe.co>
Co-authored-by: Elliot Slusky <elliot@slusky.com>
2026-08-14 17:25:23 -07:00
2 changed files with 107 additions and 5 deletions
+6 -4
View File
@@ -311,12 +311,14 @@ def chat(
# Generate response even when optional memory context is unavailable.
try:
if agent is not None:
agent_context = None
if agent_context_message is not None:
from openjarvis.agents._stubs import AgentContext
from openjarvis.agents._stubs import AgentContext
agent_context = AgentContext()
agent_context = AgentContext()
if agent_context_message is not None:
agent_context.conversation.add(agent_context_message)
for msg in history[:-1]:
if msg.role != Role.SYSTEM:
agent_context.conversation.add(msg)
response = agent.run(user_input, context=agent_context)
content = (
response.content if hasattr(response, "content") else str(response)
+101 -1
View File
@@ -17,7 +17,7 @@ from openjarvis.cli.chat_cmd import _read_input, chat
from openjarvis.core.config import JarvisConfig
from openjarvis.core.events import Event, EventBus, EventType
from openjarvis.core.registry import AgentRegistry, ToolRegistry
from openjarvis.core.types import ToolCall, ToolResult
from openjarvis.core.types import Role, ToolCall, ToolResult
from openjarvis.memory.store import LocalFactStore
from openjarvis.tools._stubs import BaseTool, ToolSpec
@@ -195,6 +195,106 @@ class TestChatAgents:
assert "simple ok" in result.output
assert "failed" not in result.output.lower()
def test_agent_receives_prior_turn_history(self) -> None:
"""Multi-turn chat must pass prior turns to agent.run() via AgentContext."""
captured_contexts: list[AgentContext | None] = []
class _CapturingAgent(BaseAgent):
agent_id = "capturing_chat_agent"
def run(self, input, context: AgentContext | None = None, **kwargs):
captured_contexts.append(context)
return AgentResult(content=f"reply-{len(captured_contexts)}", turns=1)
engine = MagicMock()
engine.engine_id = "mock"
config = JarvisConfig()
config.intelligence.default_model = "test-model"
AgentRegistry.register_value("capturing_chat_agent", _CapturingAgent)
with (
patch("openjarvis.cli.chat_cmd.load_config", return_value=config),
patch("openjarvis.engine.get_engine", return_value=("mock", engine)),
patch("openjarvis.intelligence.register_builtin_models"),
):
result = CliRunner().invoke(
chat,
["--agent", "capturing_chat_agent", "--model", "test-model"],
input="first turn\nsecond turn\n/quit\n",
)
assert result.exit_code == 0
assert len(captured_contexts) == 2
first_turn_context, second_turn_context = captured_contexts
assert first_turn_context is not None
assert first_turn_context.conversation.messages == []
assert second_turn_context is not None
prior_texts = [m.content for m in second_turn_context.conversation.messages]
assert "first turn" in prior_texts
assert "reply-1" in prior_texts
def test_agent_memory_context_precedes_prior_turn_history(self, tmp_path) -> None:
"""Memory system context must remain ahead of prior conversation turns."""
captured_contexts: list[AgentContext | None] = []
class _CapturingAgent(BaseAgent):
agent_id = "capturing_memory_chat_agent"
def run(self, input, context: AgentContext | None = None, **kwargs):
captured_contexts.append(context)
return AgentResult(content=f"reply-{len(captured_contexts)}", turns=1)
facts_path = tmp_path / "facts.jsonl"
LocalFactStore(facts_path).add("The user likes jazz", source="auto")
engine = MagicMock()
engine.engine_id = "mock"
config = JarvisConfig()
config.intelligence.default_model = "test-model"
config.memory.enabled = True
config.memory.facts_path = str(facts_path)
config.agent.context_from_memory = True
AgentRegistry.register_value(
"capturing_memory_chat_agent",
_CapturingAgent,
)
with (
patch("openjarvis.cli.chat_cmd.load_config", return_value=config),
patch("openjarvis.engine.get_engine", return_value=("mock", engine)),
patch("openjarvis.intelligence.register_builtin_models"),
patch("openjarvis.memory.build_memory_service", return_value=None),
patch("openjarvis.cli.ask._get_memory_backend", return_value=None),
):
result = CliRunner().invoke(
chat,
["--agent", "capturing_memory_chat_agent", "--model", "test-model"],
input="first turn\nsecond turn\n/quit\n",
)
assert result.exit_code == 0
assert len(captured_contexts) == 2
second_turn_context = captured_contexts[1]
assert second_turn_context is not None
messages = second_turn_context.conversation.messages
assert [message.role for message in messages] == [
Role.SYSTEM,
Role.USER,
Role.ASSISTANT,
]
assert "user likes jazz" in messages[0].content
assert [message.content for message in messages[1:]] == [
"first turn",
"reply-1",
]
def test_memory_service_started_fed_and_stopped(self) -> None:
"""The REPL starts memory, publishes each turn, and stops it."""