refactor(agent-office): drop the random idle→break→idle cycle

The pixel-office game UI is gone, so simulating coffee-break /
nap / walk states no longer serves any purpose. Remove:
- scheduler's _check_idle_breaks job (no more 60s idle scan)
- BaseAgent.check_idle_break() and _break_until field
- 'break' from VALID_STATES and from transition() branches
- IDLE_BREAK_THRESHOLD / BREAK_DURATION_MIN / BREAK_DURATION_MAX
  config knobs
- 'idle/break' guard in each agent's on_schedule (now just 'idle')

Agents now sit in 'idle' between scheduled jobs and explicit
commands. Display reads 'Idle' instead of churning between idle
and break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 08:44:50 +09:00
parent 5cde24115b
commit de8adaeadd
6 changed files with 6 additions and 36 deletions

View File

@@ -1,12 +1,9 @@
import asyncio
import random
import time import time
from typing import Optional from typing import Optional
from ..config import IDLE_BREAK_THRESHOLD, BREAK_DURATION_MIN, BREAK_DURATION_MAX
from ..db import add_log from ..db import add_log
VALID_STATES = ("idle", "working", "waiting", "reporting", "break") VALID_STATES = ("idle", "working", "waiting", "reporting")
class BaseAgent: class BaseAgent:
agent_id: str = "" agent_id: str = ""
@@ -14,7 +11,6 @@ class BaseAgent:
state: str = "idle" state: str = "idle"
state_detail: str = "" state_detail: str = ""
_idle_since: float = 0.0 _idle_since: float = 0.0
_break_until: float = 0.0
_ws_manager = None _ws_manager = None
def __init__(self): def __init__(self):
@@ -32,9 +28,6 @@ class BaseAgent:
if new_state == "idle": if new_state == "idle":
self._idle_since = time.time() self._idle_since = time.time()
elif new_state == "break":
duration = random.randint(BREAK_DURATION_MIN, BREAK_DURATION_MAX)
self._break_until = time.time() + duration
add_log(self.agent_id, f"State: {old} -> {new_state} ({detail})") add_log(self.agent_id, f"State: {old} -> {new_state} ({detail})")
@@ -48,19 +41,6 @@ class BaseAgent:
await self._ws_manager.send_notification( await self._ws_manager.send_notification(
self.agent_id, "task_completed", task_id, detail or "작업 완료" self.agent_id, "task_completed", task_id, detail or "작업 완료"
) )
if new_state == "break":
await self._ws_manager.send_agent_move(self.agent_id, "break_room")
elif old == "break" and new_state == "idle":
await self._ws_manager.send_agent_move(self.agent_id, "desk")
async def check_idle_break(self) -> None:
now = time.time()
if self.state == "idle" and (now - self._idle_since) > IDLE_BREAK_THRESHOLD:
if random.random() < 0.5:
break_type = random.choice(["커피 타임", "잠깐 산책", "졸고 있음"])
await self.transition("break", break_type)
elif self.state == "break" and now > self._break_until:
await self.transition("idle", "휴식 완료")
async def on_schedule(self) -> None: async def on_schedule(self) -> None:
raise NotImplementedError raise NotImplementedError

View File

@@ -46,7 +46,7 @@ class InstaAgent(BaseAgent):
async def on_schedule(self) -> None: async def on_schedule(self) -> None:
"""09:30 매일: 뉴스 수집 → 키워드 추출 → 텔레그램 후보 푸시. """09:30 매일: 뉴스 수집 → 키워드 추출 → 텔레그램 후보 푸시.
custom_config.auto_select=True면 카테고리당 1위 키워드 자동 슬레이트 생성.""" custom_config.auto_select=True면 카테고리당 1위 키워드 자동 슬레이트 생성."""
if self.state not in ("idle", "break"): if self.state != "idle":
return return
config = get_agent_config(self.agent_id) or {} config = get_agent_config(self.agent_id) or {}
custom = config.get("custom_config", {}) or {} custom = config.get("custom_config", {}) or {}

View File

@@ -8,7 +8,7 @@ class LottoAgent(BaseAgent):
display_name = "로또 큐레이터" display_name = "로또 큐레이터"
async def on_schedule(self) -> None: async def on_schedule(self) -> None:
if self.state not in ("idle", "break"): if self.state != "idle":
return return
await self._run(source="auto") await self._run(source="auto")

View File

@@ -44,7 +44,7 @@ class StockAgent(BaseAgent):
display_name = "주식 트레이더" display_name = "주식 트레이더"
async def on_schedule(self) -> None: async def on_schedule(self) -> None:
if self.state not in ("idle", "break"): if self.state != "idle":
return return
task_id = create_task(self.agent_id, "news_summary", {"limit": 15}) task_id = create_task(self.agent_id, "news_summary", {"limit": 15})
@@ -129,7 +129,7 @@ class StockAgent(BaseAgent):
4) status=='success' → telegram_payload.text 를 parse_mode 그대로 전송 4) status=='success' → telegram_payload.text 를 parse_mode 그대로 전송
5) 예외/실패 → 운영자에게 별도 텔레그램 알림 (HTML) 5) 예외/실패 → 운영자에게 별도 텔레그램 알림 (HTML)
""" """
if self.state not in ("idle", "break"): if self.state != "idle":
return return
task_id = create_task(self.agent_id, "screener_run", {"mode": "auto"}) task_id = create_task(self.agent_id, "screener_run", {"mode": "auto"})
@@ -243,7 +243,7 @@ class StockAgent(BaseAgent):
4) failures > 30% → 경고 알림 후 메인 메시지 발송 4) failures > 30% → 경고 알림 후 메인 메시지 발송
5) 정상 → Top 5 호재/악재 메시지 발송 (MarkdownV2) 5) 정상 → Top 5 호재/악재 메시지 발송 (MarkdownV2)
""" """
if self.state not in ("idle", "break"): if self.state != "idle":
return return
task_id = create_task(self.agent_id, "ai_news_sentiment", {}) task_id = create_task(self.agent_id, "ai_news_sentiment", {})

View File

@@ -26,11 +26,6 @@ CORS_ALLOW_ORIGINS = os.getenv(
"CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080" "CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080"
) )
# Idle break threshold (seconds)
IDLE_BREAK_THRESHOLD = int(os.getenv("IDLE_BREAK_THRESHOLD", "300")) # 5 min
BREAK_DURATION_MIN = int(os.getenv("BREAK_DURATION_MIN", "60")) # 1 min
BREAK_DURATION_MAX = int(os.getenv("BREAK_DURATION_MAX", "180")) # 3 min
# Lotto Curator # Lotto Curator
LOTTO_BACKEND_URL = os.getenv("LOTTO_BACKEND_URL", "http://lotto:8000") LOTTO_BACKEND_URL = os.getenv("LOTTO_BACKEND_URL", "http://lotto:8000")
LOTTO_CURATOR_MODEL = os.getenv("LOTTO_CURATOR_MODEL", "claude-sonnet-4-5") LOTTO_CURATOR_MODEL = os.getenv("LOTTO_CURATOR_MODEL", "claude-sonnet-4-5")

View File

@@ -5,10 +5,6 @@ from .agents import AGENT_REGISTRY
scheduler = AsyncIOScheduler(timezone="Asia/Seoul") scheduler = AsyncIOScheduler(timezone="Asia/Seoul")
async def _check_idle_breaks():
for agent in AGENT_REGISTRY.values():
await agent.check_idle_break()
async def _run_stock_schedule(): async def _run_stock_schedule():
agent = AGENT_REGISTRY.get("stock") agent = AGENT_REGISTRY.get("stock")
if agent: if agent:
@@ -78,6 +74,5 @@ def init_scheduler():
scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=0, id="lotto_curate") scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=0, id="lotto_curate")
scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=0, id="youtube_research") scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=0, id="youtube_research")
scheduler.add_job(_send_youtube_weekly_report, "cron", day_of_week="mon", hour=8, minute=0, id="youtube_weekly_report") scheduler.add_job(_send_youtube_weekly_report, "cron", day_of_week="mon", hour=8, minute=0, id="youtube_weekly_report")
scheduler.add_job(_check_idle_breaks, "interval", seconds=60, id="idle_check")
scheduler.add_job(_poll_pipelines, "interval", seconds=30, id="pipeline_poll") scheduler.add_job(_poll_pipelines, "interval", seconds=30, id="pipeline_poll")
scheduler.start() scheduler.start()