Spaces:
Running on Zero
Running on Zero
File size: 6,841 Bytes
599eb60 915c925 599eb60 915c925 599eb60 915c925 599eb60 915c925 599eb60 915c925 599eb60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | """
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
|