- compact at memory-safe token budget (~20k) not the 131k window; gpt-oss hits oMLX's prefill memory guard ~47k tokens on a 24GB machine - repair invalid_tool_calls (malformed JSON args) before echoing history — LangChain serializes the raw malformed string which oMLX 500s on - retry invokes (4x) with an oMLX watchdog restart between attempts - ALWAYS finalize: salvage picks from findings even after invoke failures Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
"""ENTRYPOINT #1 — the nightly research run.
|
|
|
|
Brings up oMLX, then runs the 3 personas strictly sequentially (one model, time-sliced),
|
|
each under a rollover wall-clock budget bounded by the hard stop. Every agent's output is
|
|
checkpointed to state/output/<trade_date>/. Run with --compressed for a fast end-to-end
|
|
test (short budgets).
|
|
|
|
uv run python -m orchestration.run_nightly [--compressed]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from loguru import logger # noqa: E402
|
|
|
|
from agents.graph import run_agent # noqa: E402
|
|
from agents.schemas import RunManifest # noqa: E402
|
|
from config.personas import PERSONAS # noqa: E402
|
|
from config.settings import get_settings # noqa: E402
|
|
from llm import server # noqa: E402
|
|
from llm.client import resolve_model_id # noqa: E402
|
|
from orchestration.budget import ( # noqa: E402
|
|
local_hard_stop_ts,
|
|
next_deadline,
|
|
next_market_trade_date,
|
|
)
|
|
from orchestration.checkpoint import Checkpointer # noqa: E402
|
|
from reporting import report_writer # noqa: E402
|
|
|
|
|
|
def main(compressed: bool = False) -> int:
|
|
s = get_settings()
|
|
s.ensure_dirs()
|
|
trade_date = next_market_trade_date(s.market_tz)
|
|
logger.add(s.log_dir / f"nightly_{trade_date}.log", rotation="10 MB", retention="14 days")
|
|
logger.info("=== nightly run trade_date={} profile={} compressed={} ===",
|
|
trade_date, s.model_profile, compressed)
|
|
|
|
if not server.ensure_ready():
|
|
logger.error("oMLX not ready; aborting.")
|
|
return 1
|
|
|
|
# preflight: confirm internet before committing to a long run (warn but continue)
|
|
try:
|
|
from tools.web import web_search
|
|
|
|
probe = web_search.invoke({"query": "stock market news today"})
|
|
logger.info("internet preflight: {}", "OK" if "http" in probe else "no results (degraded)")
|
|
except Exception as e:
|
|
logger.warning("internet preflight failed (continuing): {}", e)
|
|
|
|
model_name = resolve_model_id()
|
|
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
cp = Checkpointer(s.output_dir, trade_date)
|
|
manifest = RunManifest(
|
|
run_id=run_id,
|
|
trade_date=trade_date,
|
|
model=model_name,
|
|
started_at=datetime.now(timezone.utc),
|
|
status="running",
|
|
)
|
|
cp.write_manifest(manifest)
|
|
|
|
hard_stop = local_hard_stop_ts(s.hard_stop, s.local_tz)
|
|
per_agent_cap_s = 5 * 60 if compressed else s.per_agent_wallclock_min * 60
|
|
step_budget = 12 if compressed else s.per_agent_step_budget
|
|
min_steps = 4 if compressed else s.per_agent_min_steps
|
|
min_seconds = 60 if compressed else s.per_agent_min_minutes * 60
|
|
|
|
outputs = []
|
|
for i, persona in enumerate(PERSONAS):
|
|
now = time.time()
|
|
if now >= hard_stop:
|
|
logger.warning("hard stop reached; skipping {} remaining agents", len(PERSONAS) - i)
|
|
break
|
|
deadline = next_deadline(now, hard_stop, len(PERSONAS) - i, per_agent_cap_s)
|
|
logger.info("--- {} until {} ---", persona.id,
|
|
datetime.fromtimestamp(deadline).strftime("%H:%M:%S"))
|
|
if not server.server_up(): # recover if a prior agent left oMLX unresponsive
|
|
logger.warning("oMLX unreachable before {}; recovering...", persona.id)
|
|
server.ensure_ready(warm=False)
|
|
out = run_agent(persona, deadline_ts=deadline, step_budget=step_budget,
|
|
min_steps=min_steps, min_seconds=min_seconds,
|
|
checkpointer=cp, model_name=model_name)
|
|
cp.write(out)
|
|
outputs.append(out)
|
|
manifest.agents.append(persona.id)
|
|
try:
|
|
report_writer.write_agent_report(out, trade_date) # analyst research note
|
|
except Exception as e:
|
|
logger.warning("agent report failed for {}: {}", persona.id, e)
|
|
|
|
# combined daily report (execution results appended in the morning by run_market_open)
|
|
try:
|
|
daily = report_writer.write_daily_report(trade_date, outputs)
|
|
report_writer.to_pdf(daily)
|
|
except Exception as e:
|
|
logger.warning("daily report failed: {}", e)
|
|
|
|
manifest.status = "complete"
|
|
manifest.ended_at = datetime.now(timezone.utc)
|
|
cp.write_manifest(manifest)
|
|
logger.info("=== nightly run complete; output in {} ===", cp.dir)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(compressed="--compressed" in sys.argv))
|