Skip to content

Commit a5bf7c6

Browse files
committed
feat: add scenario records
1 parent 38039d5 commit a5bf7c6

29 files changed

Lines changed: 1693 additions & 134 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Use Shellbrain Session Start in your agent of choice. Then, just work normally.
8080

8181
**Claude Code:** *Use `/shellbrain-session-start` to get up to speed in this repo and record durable evidence-backed learnings.*
8282

83-
**Codex:** *Use `$shellbrain-session-start` to get up to speed in this repo and record durable evidence-backed learnings.*
83+
**Codex:** *Use $shellbrain-session-start to get up to speed in this repo and record durable evidence-backed learnings.*
8484

8585
**Cursor:** *Use `/shellbrain-session-start` to get up to speed in this repo and record durable evidence-backed learnings.*
8686

‎app/core/entities/scenarios.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Scenario entities for bounded problem-solving run windows."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from datetime import datetime
7+
from enum import Enum
8+
9+
10+
class ScenarioOutcome(str, Enum):
11+
"""Agent-facing terminal outcomes for one problem-solving scenario."""
12+
13+
SOLVED = "solved"
14+
ABANDONED = "abandoned"
15+
16+
17+
class ProblemRunStatus(str, Enum):
18+
"""Durable storage statuses for explicit problem-run windows."""
19+
20+
OPEN = "open"
21+
CLOSED = "closed"
22+
ABANDONED = "abandoned"
23+
24+
25+
@dataclass(frozen=True, kw_only=True)
26+
class ProblemRun:
27+
"""One bounded problem-solving scenario stored for token/ROI analysis."""
28+
29+
id: str
30+
repo_id: str
31+
status: ProblemRunStatus
32+
opened_at: datetime
33+
opened_by: str
34+
thread_id: str | None = None
35+
host_app: str | None = None
36+
host_session_key: str | None = None
37+
episode_id: str | None = None
38+
opened_event_id: str | None = None
39+
closed_at: datetime | None = None
40+
closed_by: str | None = None
41+
closed_event_id: str | None = None
42+
problem_memory_id: str | None = None
43+
solution_memory_id: str | None = None
44+
created_at: datetime | None = None
45+
updated_at: datetime | None = None
46+
47+
def __post_init__(self) -> None:
48+
"""Keep invalid run windows from crossing core boundaries."""
49+
50+
for field_name in ("id", "repo_id", "opened_by"):
51+
value = getattr(self, field_name)
52+
if not isinstance(value, str) or not value.strip():
53+
raise ValueError(f"{field_name} must be a non-empty string")
54+
if self.status == ProblemRunStatus.OPEN and self.closed_at is not None:
55+
raise ValueError("open problem runs cannot have closed_at")
56+
if self.status != ProblemRunStatus.OPEN and self.closed_at is None:
57+
raise ValueError("closed problem runs require closed_at")
58+
if self.closed_at is not None and self.closed_at < self.opened_at:
59+
raise ValueError("closed_at must be greater than or equal to opened_at")
60+
61+
62+
def outcome_to_problem_run_status(outcome: ScenarioOutcome) -> ProblemRunStatus:
63+
"""Map the agent-facing scenario outcome to durable run status."""
64+
65+
if outcome == ScenarioOutcome.SOLVED:
66+
return ProblemRunStatus.CLOSED
67+
return ProblemRunStatus.ABANDONED

‎app/core/ports/db/episode_repositories.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ def get_episode(
4040
) -> Episode | None:
4141
"""This method fetches one repo-visible episode by id."""
4242

43+
@abstractmethod
44+
def get_event(
45+
self,
46+
*,
47+
repo_id: str,
48+
episode_id: str,
49+
event_id: str,
50+
) -> EpisodeEvent | None:
51+
"""This method fetches one repo-visible event by id inside an episode."""
52+
4353
@abstractmethod
4454
def list_event_keys(self, *, episode_id: str) -> Sequence[str]:
4555
"""This method returns already-imported upstream event keys for one episode."""

‎app/core/ports/db/problem_runs.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Repository ports for explicit problem-run scenario windows."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
7+
from app.core.entities.scenarios import ProblemRun
8+
9+
10+
class IProblemRunsRepo(ABC):
11+
"""Persistence behavior for bounded problem-solving scenario records."""
12+
13+
@abstractmethod
14+
def get_by_scenario_key(
15+
self,
16+
*,
17+
repo_id: str,
18+
episode_id: str,
19+
problem_memory_id: str,
20+
opened_event_id: str,
21+
) -> ProblemRun | None:
22+
"""Return the existing scenario row for one natural idempotency key."""
23+
24+
@abstractmethod
25+
def add(self, run: ProblemRun) -> None:
26+
"""Append one explicit problem-run scenario window."""

‎app/core/ports/db/unit_of_work.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
IMemoriesRepo,
1515
IUtilityRepo,
1616
)
17+
from app.core.ports.db.problem_runs import IProblemRunsRepo
1718
from app.core.ports.embeddings.retrieval import IVectorSearch
1819
from app.core.ports.db.retrieval_repositories import (
1920
IKeywordRetrievalRepo,
@@ -37,6 +38,7 @@ class IUnitOfWork(ABC):
3738
read_policy: IReadPolicyRepo
3839
guidance: IPendingUtilityCandidatesRepo
3940
knowledge_build_runs: IKnowledgeBuildRunsRepo
41+
problem_runs: IProblemRunsRepo
4042
vector_search: IVectorSearch | None
4143

4244
@abstractmethod
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Scenario use cases for explicit problem-solving run windows."""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Record one bounded problem-solving scenario."""
2+
3+
from app.core.use_cases.scenarios.record.execute import execute_record_scenario
4+
from app.core.use_cases.scenarios.record.request import (
5+
ScenarioRecordBody,
6+
ScenarioRecordRequest,
7+
)
8+
from app.core.use_cases.scenarios.record.result import ScenarioRecordResult
9+
10+
__all__ = [
11+
"ScenarioRecordBody",
12+
"ScenarioRecordRequest",
13+
"ScenarioRecordResult",
14+
"execute_record_scenario",
15+
]

0 commit comments

Comments
 (0)