Add outbound agent response boundary

This commit is contained in:
2026-05-12 08:10:34 -05:00
parent bce93b39e0
commit 7286a241e4
2 changed files with 76 additions and 9 deletions

View File

@@ -13,10 +13,12 @@ from app.agent.modes.steward import StewardMode
from app.agent.modes.warden import WardenMode from app.agent.modes.warden import WardenMode
from app.agent.modes.librarian import LibrarianMode from app.agent.modes.librarian import LibrarianMode
from app.agent.modes.scribe import ScribeMode from app.agent.modes.scribe import ScribeMode
from app.config import settings
from app.llm.client import LLMClient from app.llm.client import LLMClient
from app.memory.database import get_session from app.memory.database import get_session
from app.memory.models import AgentActionType from app.memory.models import AgentActionType
from app.memory.repository import Repository from app.memory.repository import Repository
from app.twitch.chat import send_chat_message
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -212,6 +214,49 @@ class AgentOrchestrator:
"actions_taken": actions, "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: def set_loop_interval(self, interval_seconds: float) -> None:
"""Update how frequently the background agent loop runs.""" """Update how frequently the background agent loop runs."""
if interval_seconds < 1: if interval_seconds < 1:
@@ -269,20 +314,17 @@ class AgentOrchestrator:
theme=session_info.get("theme") theme=session_info.get("theme")
) )
session_info["last_hearthkeeper_prompt_at"] = datetime.utcnow() session_info["last_hearthkeeper_prompt_at"] = datetime.utcnow()
delivery = await self.emit_agent_response(
async for db_session in get_session():
repo = Repository(db_session)
await repo.record_action(
session_id=session_id, session_id=session_id,
action_type=AgentActionType.RESPONSE, message=agent_response,
mode="hearthkeeper", mode="hearthkeeper",
description=agent_response,
) )
return { return {
"session_id": session_id, "session_id": session_id,
"actions_taken": ["HEARTHKEEPER_PROMPT"], "actions_taken": ["HEARTHKEEPER_PROMPT"],
"agent_response": agent_response, "agent_response": agent_response,
"delivery": delivery,
"reason": "inactive_chat", "reason": "inactive_chat",
} }
except Exception as e: except Exception as e:

View File

@@ -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") @app.get("/admin/ledger")
async def get_ledger(session_id: str) -> dict: async def get_ledger(session_id: str) -> dict:
"""Get the markdown ledger for a session.""" """Get the markdown ledger for a session."""