From 7286a241e44a397fdaa48e36cc746404831e92ce Mon Sep 17 00:00:00 2001 From: Ken Schaefer Date: Tue, 12 May 2026 08:10:34 -0500 Subject: [PATCH] Add outbound agent response boundary --- app/agent/orchestrator.py | 60 +++++++++++++++++++++++++++++++++------ app/main.py | 25 ++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/app/agent/orchestrator.py b/app/agent/orchestrator.py index eea9fa1..3eb3407 100644 --- a/app/agent/orchestrator.py +++ b/app/agent/orchestrator.py @@ -13,10 +13,12 @@ from app.agent.modes.steward import StewardMode from app.agent.modes.warden import WardenMode from app.agent.modes.librarian import LibrarianMode from app.agent.modes.scribe import ScribeMode +from app.config import settings from app.llm.client import LLMClient from app.memory.database import get_session from app.memory.models import AgentActionType from app.memory.repository import Repository +from app.twitch.chat import send_chat_message logger = logging.getLogger(__name__) @@ -212,6 +214,49 @@ class AgentOrchestrator: "actions_taken": actions, } + async def emit_agent_response( + self, + session_id: str, + message: str, + mode: str, + triggered_by_message_id: str | None = None, + ) -> dict: + """Send an agent response through the outbound chat boundary.""" + session_info = self.active_sessions.get(session_id) + if not session_info: + logger.warning(f"Session {session_id} not found") + return {"sent": False, "reason": "session_not_found"} + + channel_name = session_info["channel_name"] + sent = await send_chat_message(channel_name=channel_name, message=message) + bot_username = settings.TWITCH_BOT_USERNAME or "sanctum_chronicler" + + async for db_session in get_session(): + repo = Repository(db_session) + bot_message_id = await repo.add_chat_message( + session_id=session_id, + username=bot_username, + content=message, + is_bot=True, + ) + action_id = await repo.record_action( + session_id=session_id, + action_type=AgentActionType.RESPONSE, + mode=mode, + description=message, + triggered_by_message_id=triggered_by_message_id, + ) + + logger.info( + f"Agent response emitted. Session: {session_id}, Mode: {mode}, Sent: {sent}" + ) + return { + "sent": sent, + "channel": channel_name, + "message_id": bot_message_id, + "action_id": action_id, + } + def set_loop_interval(self, interval_seconds: float) -> None: """Update how frequently the background agent loop runs.""" if interval_seconds < 1: @@ -269,20 +314,17 @@ class AgentOrchestrator: theme=session_info.get("theme") ) session_info["last_hearthkeeper_prompt_at"] = datetime.utcnow() - - async for db_session in get_session(): - repo = Repository(db_session) - await repo.record_action( - session_id=session_id, - action_type=AgentActionType.RESPONSE, - mode="hearthkeeper", - description=agent_response, - ) + delivery = await self.emit_agent_response( + session_id=session_id, + message=agent_response, + mode="hearthkeeper", + ) return { "session_id": session_id, "actions_taken": ["HEARTHKEEPER_PROMPT"], "agent_response": agent_response, + "delivery": delivery, "reason": "inactive_chat", } except Exception as e: diff --git a/app/main.py b/app/main.py index 200ce55..486945f 100644 --- a/app/main.py +++ b/app/main.py @@ -128,6 +128,31 @@ async def test_message(session_id: str = Form(...), message: str = Form(...), us } +@app.post("/admin/test-agent-response") +async def test_agent_response( + session_id: str = Form(...), + message: str = Form(...), + mode: str = Form("admin"), +) -> dict: + """Send a test agent response through the outbound boundary.""" + if not orchestrator: + raise HTTPException(status_code=503, detail="Orchestrator not initialized") + + delivery = await orchestrator.emit_agent_response( + session_id=session_id, + message=message, + mode=mode, + ) + if not delivery.get("sent"): + raise HTTPException(status_code=404, detail=delivery.get("reason", "send_failed")) + + return { + "status": "agent_response_emitted", + "delivery": delivery, + "timestamp": datetime.utcnow().isoformat(), + } + + @app.get("/admin/ledger") async def get_ledger(session_id: str) -> dict: """Get the markdown ledger for a session."""