diff --git a/src/praisonai/praisonai/cli/commands/run.py b/src/praisonai/praisonai/cli/commands/run.py index 1d0b7db12..95b0f4125 100644 --- a/src/praisonai/praisonai/cli/commands/run.py +++ b/src/praisonai/praisonai/cli/commands/run.py @@ -370,13 +370,20 @@ def _run_prompt( approval, all_tools=approve_all_tools, timeout=approval_timeout, ) - # Add session support to Agent if needed - if session_id: - agent_config["resume_session"] = session_id - if auto_save_name: - agent_config["auto_save"] = auto_save_name + # Session continuity via MemoryConfig (Agent has no resume_session param) + continuity_id = session_id or auto_save_name + if continuity_id: + from praisonaiagents import MemoryConfig + from ..state.project_sessions import apply_cli_session_continuity + agent_config["memory"] = MemoryConfig( + session_id=continuity_id, + auto_save=auto_save_name or continuity_id, + history=bool(session_id), + ) agent = Agent(**agent_config) + if continuity_id: + apply_cli_session_continuity(agent, continuity_id) result = agent.start(prompt) output.emit_result( @@ -440,44 +447,6 @@ class Args: praison.args = args - # If output_mode is "actions", use direct Agent with actions preset - if output_mode == "actions": - from praisonaiagents import Agent - - agent_config = { - "name": "RunAgent", - "role": "Assistant", - "goal": "Complete the task", - "output": "actions", # Use actions preset - } - if model: - agent_config["llm"] = model - - # Resolve approval backend if specified - if approval: - from praisonai.cli.features.approval import resolve_approval_config - agent_config["approval"] = resolve_approval_config( - approval, all_tools=approve_all_tools, timeout=approval_timeout, - ) - - # Add session support to Agent if needed - if session_id: - agent_config["resume_session"] = session_id - if auto_save_name: - agent_config["auto_save"] = auto_save_name - - agent = Agent(**agent_config) - result = agent.start(prompt) - - output.emit_result( - message="Prompt completed", - data={"result": str(result) if result else None} - ) - - # Don't print result again - actions mode already shows output - return - - # Use handle_direct_prompt for other modes result = praison.handle_direct_prompt(prompt) output.emit_result( diff --git a/src/praisonai/praisonai/cli/main.py b/src/praisonai/praisonai/cli/main.py index 370498d8b..25e71469c 100644 --- a/src/praisonai/praisonai/cli/main.py +++ b/src/praisonai/praisonai/cli/main.py @@ -4513,6 +4513,33 @@ def handle_direct_prompt(self, prompt): from praisonaiagents import MemoryConfig agent_config["memory"] = MemoryConfig(auto_save=self.args.auto_save) print(f"[bold cyan]Auto-save enabled - session will be saved as '{self.args.auto_save}'[/bold cyan]") + + resume_session_id = getattr(self.args, 'resume_session', None) + if resume_session_id: + from praisonaiagents import MemoryConfig + save_name = getattr(self.args, 'auto_save', None) or resume_session_id + + # Preserve existing memory configuration fields + existing_memory = agent_config.get("memory") + if isinstance(existing_memory, MemoryConfig): + existing_memory.session_id = resume_session_id + existing_memory.auto_save = save_name + existing_memory.history = True + elif isinstance(existing_memory, dict): + # Convert dict to MemoryConfig while preserving fields + existing_memory.update({ + "session_id": resume_session_id, + "auto_save": save_name, + "history": True, + }) + agent_config["memory"] = MemoryConfig(**existing_memory) + else: + agent_config["memory"] = MemoryConfig( + session_id=resume_session_id, + auto_save=save_name, + history=True, + ) + print(f"[bold cyan]Resuming session '{resume_session_id}'[/bold cyan]") if getattr(self.args, 'history', None): if agent_config.get("memory") is None: @@ -4792,6 +4819,15 @@ def level_based_approve(function_name, arguments, risk_level): flow.display_workflow_start("Direct Prompt", ["DirectAgent"]) agent = PraisonAgent(**agent_config) + + continuity_id = None + if hasattr(self, 'args'): + continuity_id = getattr(self.args, 'resume_session', None) or getattr( + self.args, 'auto_save', None + ) + if continuity_id: + from praisonai.cli.state.project_sessions import apply_cli_session_continuity + apply_cli_session_continuity(agent, continuity_id) # AutoRag - Automatic RAG retrieval decision if hasattr(self, 'args') and getattr(self.args, 'auto_rag', False): diff --git a/src/praisonai/praisonai/cli/state/project_sessions.py b/src/praisonai/praisonai/cli/state/project_sessions.py index afdd0e370..681c0a596 100644 --- a/src/praisonai/praisonai/cli/state/project_sessions.py +++ b/src/praisonai/praisonai/cli/state/project_sessions.py @@ -98,4 +98,26 @@ def find_last_session(project_path: Optional[str] = None) -> Optional[str]: Session ID or None if no sessions exist """ store = get_project_session_store(project_path) - return store.get_last_session_id() \ No newline at end of file + return store.get_last_session_id() + + +def apply_cli_session_continuity(agent, session_id: str, project_path: Optional[str] = None) -> None: + """ + Bind an agent to the project-scoped session store and restore prior turns. + + CLI ``run --continue/--session`` discovers sessions in the project store but + agents default to the global store unless explicitly wired here. + """ + store = get_project_session_store(project_path) + agent._session_store = store + agent._session_id = session_id + if not getattr(agent, "auto_save", None): + agent.auto_save = session_id + + history = store.get_chat_history(session_id) + if history and not agent.chat_history: + agent.chat_history = [ + {"role": msg.get("role", "user"), "content": msg.get("content", "")} + for msg in history + ] + agent._auto_save_last_index = len(agent.chat_history) \ No newline at end of file diff --git a/src/praisonai/tests/unit/cli/test_run_session_continuity.py b/src/praisonai/tests/unit/cli/test_run_session_continuity.py new file mode 100644 index 000000000..7830eb95f --- /dev/null +++ b/src/praisonai/tests/unit/cli/test_run_session_continuity.py @@ -0,0 +1,185 @@ +"""Regression tests for CLI run session continuity.""" + +import tempfile +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from praisonaiagents import Agent, MemoryConfig + + +def test_agent_rejects_resume_session_kwarg(): + """Agent must not accept the invalid resume_session parameter.""" + with pytest.raises(TypeError, match="resume_session"): + Agent(name="RunAgent", resume_session="abc123") + + +def test_apply_cli_session_continuity_restores_project_history(): + """Project-scoped history must be restored into the agent chat_history.""" + from praisonai.cli.state.project_sessions import ( + apply_cli_session_continuity, + get_project_session_store, + ) + + with tempfile.TemporaryDirectory() as tmp: + project_path = Path(tmp) + store = get_project_session_store(str(project_path)) + session_id = "continuity-test-session" + store.add_user_message(session_id, "remember the code is 42") + store.add_assistant_message(session_id, "Got it, the code is 42.") + + agent = Agent(name="RunAgent") + + apply_cli_session_continuity(agent, session_id, project_path=str(project_path)) + + assert agent._session_store.session_dir == store.session_dir + assert agent._session_id == session_id + assert len(agent.chat_history) == 2 + assert agent.chat_history[0]["content"] == "remember the code is 42" + assert agent._auto_save_last_index == 2 + + +def test_handle_direct_prompt_wires_project_session(monkeypatch): + """handle_direct_prompt must wire resume_session to the project store.""" + from praisonai.cli.main import PraisonAI + + captured = {} + + class FakeAgent: + def __init__(self, **kwargs): + self.chat_history = [] + self._session_store = None + self._session_id = None + self.auto_save = None + self._auto_save_last_index = 0 + captured["agent_config"] = kwargs + + def start(self, prompt): + captured["prompt"] = prompt + return "ok" + + monkeypatch.setattr("praisonaiagents.Agent", FakeAgent) + monkeypatch.setattr( + "praisonai.cli.state.project_sessions.apply_cli_session_continuity", + lambda agent, session_id, project_path=None: captured.setdefault( + "continuity", session_id + ), + ) + monkeypatch.setattr( + "praisonai.cli.main.PraisonAI._execute_agent_with_budget_handling", + lambda self, agent, method, prompt: agent.start(prompt), + ) + monkeypatch.setattr( + "praisonai.cli.main.PraisonAI._resolve_display_mode", + lambda self: "quiet", + ) + monkeypatch.setattr( + "praisonai.cli.main.PraisonAI._rewrite_query_if_enabled", + lambda self, prompt: prompt, + ) + monkeypatch.setattr( + "praisonai.cli.main.PraisonAI._expand_prompt_if_enabled", + lambda self, prompt: prompt, + ) + monkeypatch.setattr("praisonai.cli.main.PRAISONAI_AVAILABLE", True) + + praison = PraisonAI() + args = SimpleNamespace( + profile=False, + workflow=None, + no_rules=True, + verbose=0, + llm=None, + memory=False, + auto_save="saved-session", + resume_session="saved-session", + history=None, + tools=None, + toolset=None, + no_tools=True, + web_search=False, + web_fetch=False, + prompt_caching=False, + autonomy=None, + approval=None, + router=False, + metrics=False, + telemetry=False, + mcp=None, + auto_rag=False, + image=None, + image_generate=False, + cli_backend=None, + flow_display=False, + planning=False, + query_rewrite=False, + expand_prompt=False, + tool_timeout=None, + tool_retry_attempts=1, + rewrite_tools=None, + expand_tools=None, + planning_tools=None, + ) + praison.args = args + + result = praison.handle_direct_prompt("follow up question") + + assert result == "ok" + memory = captured["agent_config"]["memory"] + assert isinstance(memory, MemoryConfig) + assert memory.session_id == "saved-session" + assert memory.history is True + assert captured["continuity"] == "saved-session" + + +def test_run_prompt_actions_mode_wires_session_continuity(monkeypatch): + """Actions mode must wire resume_session to project session continuity.""" + from praisonai.cli.commands.run import _run_prompt + + captured = {} + + class FakeAgent: + def __init__(self, **kwargs): + self.chat_history = [] + self._session_store = None + self._session_id = None + self.auto_save = None + self._auto_save_last_index = 0 + captured["agent_config"] = kwargs + + def start(self, prompt): + captured["prompt"] = prompt + return "actions complete" + + monkeypatch.setattr("praisonaiagents.Agent", FakeAgent) + monkeypatch.setattr( + "praisonai.cli.state.project_sessions.apply_cli_session_continuity", + lambda agent, session_id, project_path=None: captured.setdefault("continuity", session_id), + ) + + # Mock output controller + class MockOutput: + def emit_result(self, result, status="success", metadata=None): + captured["output_result"] = result + + def print_info(self, msg): + pass + + def print_warning(self, msg): + pass + + monkeypatch.setattr("praisonai.cli.commands.run.get_output_controller", lambda: MockOutput()) + + result = _run_prompt( + "follow up question", + output_mode="actions", + session="saved-session", + ) + + memory = captured["agent_config"]["memory"] + assert isinstance(memory, MemoryConfig) + assert memory.session_id == "saved-session" + assert memory.history is True + assert captured["continuity"] == "saved-session" + assert captured["prompt"] == "follow up question"