Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,31 @@ Feature-complete across the planned phases:
change Curated state. Ask for `preview_curated` with `developing_stories=true` to inspect the
bounded story-memory path. It follows confirmed sent reports across source updates, records
explicit publish/update/combine/hold/skip decisions, and stays disabled until
`CURATED_DEVELOPING_STORIES=true`. Story drafts and the one allowed revision have a fixed 4,096
`CURATED_DEVELOPING_STORIES=true`. With the default `CURATED_REQUEST_PROFILE=legacy`, story
drafts and the one allowed revision have a fixed 4,096
token output ceiling; legacy Curated drafts and reviews keep the 900-token ceiling, and every
call spends from the same Curated daily token and dollar budgets. Set `CURATED_CHANNEL_ID` only
after reviewing preview quality. See
[ADR-0012](docs/decisions/0012-scout-is-the-item-source.md) for the Scout boundary. Scout currently
fetches only newly reported items; [Scout #21](https://github.com/R055LE/scout/issues/21) tracks
same-page refresh evidence.

`CURATED_REQUEST_PROFILE=sol-astra-v1` requires developing stories and selects the evaluated
Sol 20260929 writer (16,384 output tokens, 1,200-second timeout) and Astra 20260903 reviewer
(8,192 output tokens, 900-second timeout). Both use OpenAI, default service tier, medium
reasoning, no temperature, and one transport attempt with no provider fallback. Response
caching is disabled; prompt caching uses explicit mode with no cache markers. Unknown cache
usage stays unknown. Reported cache activity, a mismatched route, or an unusable response
prevents publication; trustworthy usage is recorded even on rejected responses. Missing usage
and transport failures are recorded as uncertain, without retaining response text. Selected
profiles reject legacy Curated calls and do not automatically replay uncertain calls.

The scheduled job keeps its 30-minute overall lease and deadline, which can interrupt the
four-call workflow before all per-call timeouts elapse. The selector defaults to `legacy` and
does not change deployed channels, schedules, or budgets. Existing budget checks compare
already-reported usage, so they are not hard spend reservations; [#117](https://github.com/R055LE/roger/issues/117)
tracks reservation enforcement before activating this profile in production.

Runs as a non-root, read-only-rootfs container. More than 300 tests cover the guard rules, the tool loop
(including channel creation with access presets and the confirm-gated edit, post, and reorder
tools), the rate limiter, and the Scout ingestion path.
Expand Down
1 change: 1 addition & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ services:
MODEL_GIGABRAIN: ${MODEL_GIGABRAIN:-}
MODEL_CURATED: ${MODEL_CURATED:-}
MODEL_CURATED_REVIEW: ${MODEL_CURATED_REVIEW:-}
CURATED_REQUEST_PROFILE: ${CURATED_REQUEST_PROFILE:-legacy}
DAILY_TOKENS_ADMIN: ${DAILY_TOKENS_ADMIN:-150000}
DAILY_TOKENS_AMBIENT: ${DAILY_TOKENS_AMBIENT:-40000}
DAILY_TOKENS_GIGABRAIN: ${DAILY_TOKENS_GIGABRAIN:-100000}
Expand Down
2 changes: 2 additions & 0 deletions roger.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ MODEL_GIGABRAIN=anthropic/claude-opus-4.5,openai/gpt-5,google/gemini-2.5-pro
MODEL_CURATED=google/gemini-2.5-flash-lite,anthropic/claude-haiku-4.5
# independent fact/excerpt review; no Curated draft can post when this is blank
MODEL_CURATED_REVIEW=google/gemini-2.5-pro
# Opt-in story profiles; requires CURATED_DEVELOPING_STORIES=true.
CURATED_REQUEST_PROFILE=legacy

# --- budgets (daily in+out tokens per brain) ---
DAILY_TOKENS_ADMIN=150000
Expand Down
10 changes: 9 additions & 1 deletion roger/brains/curated.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from openai import OpenAIError

from roger.identity import ROGER_IDENTITY
from roger.llm import LLM, BudgetExceeded, LLMConfigError
from roger.llm import LLM, BudgetExceeded, CuratedModelError, LLMConfigError
from roger.scout_source import collect_from_scout
from roger.store import CURATED_CHECK_LEASE_SECONDS, Store

Expand Down Expand Up @@ -555,7 +555,13 @@ async def finish(status: str, *, retry_at: float = 0) -> None:
except LLMConfigError:
await finish("configuration")
return await result("curated brain not configured")
except CuratedModelError as exc:
await finish("rejected")
return await result(f"model response rejected: {exc.status}; skipped")
except (OpenAIError, TimeoutError):
if getattr(settings, "curated_request_profile", "legacy") != "legacy":
await finish("rejected")
return await result("model request interrupted; usage uncertain; skipped")
log.exception("curated model request failed")
await finish("transient", retry_at=clock().timestamp() + 3600)
return await result("model request failed; skipped")
Expand Down Expand Up @@ -684,6 +690,8 @@ async def preview_curated_job(
return {"status": "budget exceeded; skipped"}
except LLMConfigError:
return {"status": "curated brain not configured"}
except CuratedModelError as exc:
return {"status": f"model response rejected: {exc.status}; skipped"}
except OpenAIError:
log.exception("developing story preview model request failed")
return {"status": "model request failed; skipped"}
Expand Down
3 changes: 3 additions & 0 deletions roger/brains/stories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,9 @@ def input_fingerprint(
DECISION_RESPONSE_FORMAT,
REVIEW_RESPONSE_FORMAT,
]
profile = getattr(settings, "curated_request_profile", "legacy")
if profile != "legacy":
payload.append({"request_profile": profile})
return hashlib.sha256(
json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()
).hexdigest()
Expand Down
16 changes: 15 additions & 1 deletion roger/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from __future__ import annotations

import pathlib
from typing import Literal
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from pydantic import Field, field_validator
from pydantic import Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from roger.curated_protocol import CURATED_REVIEWER, CURATED_WRITER


def _split_csv(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
Expand Down Expand Up @@ -40,6 +43,7 @@ class Settings(BaseSettings):
model_gigabrain: str = ""
model_curated: str = ""
model_curated_review: str = ""
curated_request_profile: Literal["legacy", "sol-astra-v1"] = "legacy"

# --- budgets (daily in+out tokens per brain) ---
daily_tokens_admin: int = Field(default=150_000, gt=0)
Expand Down Expand Up @@ -137,6 +141,12 @@ def _timezone_must_be_loadable(cls, value: str) -> str:
raise ValueError("must name a loadable IANA timezone") from exc
return value

@model_validator(mode="after")
def _curated_profile_requires_stories(self) -> Settings:
if self.curated_request_profile != "legacy" and not self.curated_developing_stories:
raise ValueError("CURATED_REQUEST_PROFILE requires CURATED_DEVELOPING_STORIES")
return self

@property
def admin_models(self) -> list[str]:
return _split_csv(self.model_admin)
Expand All @@ -151,10 +161,14 @@ def gigabrain_models(self) -> list[str]:

@property
def curated_models(self) -> list[str]:
if self.curated_request_profile != "legacy":
return [CURATED_WRITER.model]
return _split_csv(self.model_curated)

@property
def curated_review_models(self) -> list[str]:
if self.curated_request_profile != "legacy":
return [CURATED_REVIEWER.model]
return _split_csv(self.model_curated_review)

@property
Expand Down
Loading
Loading