Agentic_SRE / memory /write.py
LordofMonarchs's picture
Fix auto-registration of tasks, add offline DB fallback for local evaluation, and improve rate-limit out-of-credit failover
915c925
Raw
History Blame Contribute Delete
6.84 kB
"""
memory/write.py β€” Per-decision write path.
Key rules (from brief Β§6):
- Vector entry (decisions row) and any related causal_edges rows from the
same decision MUST be written in a single transaction β€” no partial writes.
- credit_label starts NULL and is backfilled by credit_assignment.py post-episode.
- quarantine_reason must be the gate's actual textual reason, not just the flag.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
import structlog
from sqlalchemy import select
from memory.db import get_db_session
from memory.models import CausalEdge, Decision, Episode
log = structlog.get_logger(__name__)
# ── Episode lifecycle ─────────────────────────────────────────────────────────
async def open_episode(
episode_id: str,
task_id: str,
agent_version: str,
started_at: float,
) -> None:
"""Create the episode row at the start of an episode."""
started_dt = datetime.utcfromtimestamp(started_at)
try:
async with get_db_session() as session:
episode = Episode(
episode_id=episode_id,
task_id=task_id,
agent_version=agent_version,
started_at=started_dt,
)
session.add(episode)
log.info("memory.episode.opened", episode_id=episode_id, task_id=task_id)
except Exception as exc:
log.warning("memory.db_unavailable", op="open_episode", error=str(exc), note="Skipping DB write, continuing in-memory")
async def close_episode(
episode_id: str,
outcome: str,
final_reward: float,
resolution_summary: str,
ended_at: float,
) -> None:
"""Update episode row with outcome after it completes."""
ended_dt = datetime.utcfromtimestamp(ended_at)
try:
async with get_db_session() as session:
result = await session.execute(
select(Episode).where(Episode.episode_id == episode_id)
)
episode = result.scalar_one_or_none()
if episode is None:
log.warning("memory.episode.not_found", episode_id=episode_id)
return
episode.ended_at = ended_dt
episode.outcome = outcome
episode.final_reward = final_reward
episode.resolution_summary = resolution_summary
session.add(episode)
log.info("memory.episode.closed", episode_id=episode_id, outcome=outcome, reward=final_reward)
except Exception as exc:
log.warning("memory.db_unavailable", op="close_episode", error=str(exc), note="Skipping DB write")
# ── Per-decision write ────────────────────────────────────────────────────────
async def write_decision(
episode_id: str,
step_index: int,
state_signature: str,
state_embedding: Optional[List[float]],
action_type: str,
action_payload: Dict[str, Any],
result_stdout: Optional[str] = None,
result_stderr: Optional[str] = None,
exit_code: Optional[int] = None,
quarantine_flag: bool = False,
no_match_flag: bool = False,
quarantine_reason: Optional[str] = None,
causal_edges: Optional[List[Dict[str, Any]]] = None,
) -> str:
"""
Write a decision row + any causal edges in a SINGLE transaction.
Returns the decision_id for use in subsequent edge writes.
causal_edges: list of dicts with keys:
- to_decision (str, UUID of subsequent decision β€” may be None if not yet known)
- relation_type (str): 'caused' | 'preceded' | 'remediated_by'
- confidence (float): default 1.0
Note: edges where to_decision is not yet known are written with from_decision
and the to_decision is backfilled by the caller after the next decision is written.
"""
decision_id = str(uuid.uuid4())
try:
async with get_db_session() as session:
decision = Decision(
decision_id=decision_id,
episode_id=episode_id,
step_index=step_index,
state_signature=state_signature,
state_embedding=state_embedding,
action_type=action_type,
action_payload=action_payload,
result_stdout=result_stdout,
result_stderr=result_stderr,
exit_code=exit_code,
quarantine_flag=quarantine_flag,
no_match_flag=no_match_flag,
quarantine_reason=quarantine_reason,
# credit_label starts NULL β€” backfilled post-episode
)
session.add(decision)
# Write causal edges in the SAME transaction
if causal_edges:
for edge_spec in causal_edges:
to_decision = edge_spec.get("to_decision")
if to_decision is None:
continue # Skip incomplete edges; caller backfills
edge = CausalEdge(
edge_id=str(uuid.uuid4()),
from_decision=decision_id,
to_decision=to_decision,
relation_type=edge_spec.get("relation_type", "preceded"),
confidence=float(edge_spec.get("confidence", 1.0)),
last_reinforced=datetime.utcnow(),
)
session.add(edge)
log.debug(
"memory.decision.written",
decision_id=decision_id,
episode_id=episode_id,
step_index=step_index,
action_type=action_type,
quarantine_flag=quarantine_flag,
)
except Exception as exc:
log.warning("memory.db_unavailable", op="write_decision", error=str(exc), note="Skipping DB write")
return decision_id
async def write_causal_edge(
from_decision: str,
to_decision: str,
relation_type: str = "preceded",
confidence: float = 1.0,
) -> str:
"""Write a single causal edge (used when to_decision becomes known after the fact)."""
edge_id = str(uuid.uuid4())
try:
async with get_db_session() as session:
edge = CausalEdge(
edge_id=edge_id,
from_decision=from_decision,
to_decision=to_decision,
relation_type=relation_type,
confidence=confidence,
last_reinforced=datetime.utcnow(),
)
session.add(edge)
except Exception as exc:
log.warning("memory.db_unavailable", op="write_causal_edge", error=str(exc))
return edge_id