generated from coulomb/repo-seed
Implement WARDEN-WP-0024 experiential memory and agent sessions.
Add phase-memory bridge, warden memory CLI, route/access/sign recording, memory-aware worker planning with OpenRouter skip, tests, wiki, and AGENTS.md orientation for Claude, Codex, Grok, and future agent sessions.
This commit is contained in:
@@ -44,6 +44,11 @@ activity_app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(activity_app, name="activity")
|
||||
memory_app = typer.Typer(
|
||||
help="Cross-runtime experiential memory via phase-memory (WARDEN-WP-0024)",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(memory_app, name="memory")
|
||||
|
||||
console = Console()
|
||||
err = Console(stderr=True)
|
||||
@@ -53,6 +58,30 @@ err = Console(stderr=True)
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _record_memory_episode(
|
||||
*,
|
||||
command: str,
|
||||
outcome: str,
|
||||
need: str = "",
|
||||
route_id: str = "",
|
||||
) -> None:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return
|
||||
try:
|
||||
warden_memory.record_command_episode(
|
||||
command=command,
|
||||
outcome=outcome,
|
||||
need=need,
|
||||
route_id=route_id,
|
||||
)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
|
||||
def _load_cfg() -> WardenConfig:
|
||||
try:
|
||||
return load_config()
|
||||
@@ -128,6 +157,12 @@ def sign(
|
||||
|
||||
# cert_command interface: write cert text to stdout only
|
||||
print(record.cert_path.read_text().strip())
|
||||
_record_memory_episode(
|
||||
command="sign",
|
||||
outcome="resolved",
|
||||
need=f"ssh cert {actor_name}",
|
||||
route_id="ops-warden-ssh-cert",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -753,14 +788,30 @@ def route_find(
|
||||
|
||||
if output_json:
|
||||
print(json.dumps([_entry_summary(e) for e in matches], indent=2))
|
||||
if matches:
|
||||
_record_memory_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need=query,
|
||||
route_id=matches[0].id,
|
||||
)
|
||||
else:
|
||||
_record_memory_episode(command="route find", outcome="skipped", need=query)
|
||||
return
|
||||
|
||||
if not matches:
|
||||
_record_memory_episode(command="route find", outcome="skipped", need=query)
|
||||
console.print(
|
||||
f"No routing match for {query!r}. "
|
||||
"Try `warden route list --all` to browse all scenarios."
|
||||
)
|
||||
return
|
||||
_record_memory_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need=query,
|
||||
route_id=matches[0].id,
|
||||
)
|
||||
_print_entry_table(matches, f"Matches for {query!r}")
|
||||
|
||||
|
||||
@@ -1003,8 +1054,20 @@ def access(
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(_access_json(entry, expanded, gate, domain), indent=2))
|
||||
_record_memory_episode(
|
||||
command="access",
|
||||
outcome="resolved",
|
||||
need=need,
|
||||
route_id=entry.id,
|
||||
)
|
||||
return
|
||||
|
||||
_record_memory_episode(
|
||||
command="access",
|
||||
outcome="resolved",
|
||||
need=need,
|
||||
route_id=entry.id,
|
||||
)
|
||||
console.print(f"[bold]{entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
|
||||
console.print(f" owner : {entry.owner_repo} ({entry.subsystem})")
|
||||
|
||||
@@ -1305,3 +1368,58 @@ def worker_status_cmd() -> None:
|
||||
console.print(f"timer : {st or 'unknown'}")
|
||||
except Exception: # noqa: BLE001 — systemd may be absent (cron/other host)
|
||||
console.print("timer : (systemd not available)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# warden memory — cross-runtime experiential memory (WARDEN-WP-0024)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@memory_app.command("status")
|
||||
def memory_status(
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Show canonical phase-memory store status (metadata only)."""
|
||||
from warden import memory as warden_memory
|
||||
|
||||
if not warden_memory.memory_available():
|
||||
err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]")
|
||||
raise typer.Exit(2)
|
||||
try:
|
||||
payload = warden_memory.status()
|
||||
except RuntimeError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
if output_json:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
console.print(f"store_path : {payload.get('store_path', '')}")
|
||||
console.print(f"profile_id : {payload.get('profile_id', '')}")
|
||||
console.print(f"episode_count : {payload.get('episode_count', 0)}")
|
||||
console.print(f"session_kinds : {payload.get('episode_counts_by_session_kind', {})}")
|
||||
console.print(f"last_activation: {payload.get('last_activation_at') or '—'}")
|
||||
|
||||
|
||||
@memory_app.command("activate")
|
||||
def memory_activate(
|
||||
need: Annotated[str, typer.Option("--need", help="Optional routing need fingerprint source")] = "",
|
||||
agent: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--agent", help="Agent id for session_kind warden.agent.<id> (claude, codex, grok, …)"),
|
||||
] = None,
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Activate bounded coordination memory for worker, operator, or agent sessions."""
|
||||
from warden import memory as warden_memory
|
||||
|
||||
if not warden_memory.memory_available():
|
||||
err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]")
|
||||
raise typer.Exit(2)
|
||||
try:
|
||||
payload = warden_memory.activate(need=need, agent=agent)
|
||||
except RuntimeError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
if output_json:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
console.print(warden_memory.format_activation_summary(payload))
|
||||
|
||||
122
src/warden/memory.py
Normal file
122
src/warden/memory.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""phase-memory bridge for ops-warden cross-runtime experiential memory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Mapping, Optional
|
||||
|
||||
_PHASE_MEMORY_ERROR = (
|
||||
"phase-memory is required for warden memory commands. "
|
||||
"Install with: pip install phase-memory (or set PYTHONPATH to phase-memory/src)."
|
||||
)
|
||||
|
||||
|
||||
def _phase_memory():
|
||||
try:
|
||||
import phase_memory.ops_warden as pm
|
||||
|
||||
return pm
|
||||
except ImportError as exc: # pragma: no cover - exercised via tests with PYTHONPATH
|
||||
raise RuntimeError(_PHASE_MEMORY_ERROR) from exc
|
||||
|
||||
|
||||
def memory_available() -> bool:
|
||||
try:
|
||||
_phase_memory()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
|
||||
def enabled(environ: Mapping[str, str] | None = None) -> bool:
|
||||
environ = environ or os.environ
|
||||
return str(environ.get("WARDEN_MEMORY", "1")).strip().lower() not in {"0", "false", "no", "off"}
|
||||
|
||||
|
||||
def store_path(environ: Mapping[str, str] | None = None):
|
||||
return _phase_memory().default_memory_store_path(environ)
|
||||
|
||||
|
||||
def session_kind(environ: Mapping[str, str] | None = None) -> str:
|
||||
return _phase_memory().resolve_session_kind(environ)
|
||||
|
||||
|
||||
def status(environ: Mapping[str, str] | None = None) -> dict[str, Any]:
|
||||
pm = _phase_memory()
|
||||
return pm.OpsWardenMemoryStore.open(environ=environ).status()
|
||||
|
||||
|
||||
def activate(
|
||||
*,
|
||||
need: str = "",
|
||||
agent: Optional[str] = None,
|
||||
session_id: str = "",
|
||||
environ: Mapping[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
pm = _phase_memory()
|
||||
env = dict(environ or os.environ)
|
||||
if agent:
|
||||
env["WARDEN_AGENT_ID"] = agent
|
||||
kind = pm.resolve_session_kind(env)
|
||||
return pm.activate_ops_warden_memory(
|
||||
pm.OpsWardenMemoryStore.open(environ=env),
|
||||
session_kind=kind,
|
||||
need=need,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
|
||||
def record_command_episode(
|
||||
*,
|
||||
command: str,
|
||||
outcome: str,
|
||||
need: str = "",
|
||||
route_id: str = "",
|
||||
diagnostic_codes: Optional[list[str]] = None,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if not enabled(environ):
|
||||
return {"valid": True, "skipped": True, "reason": "WARDEN_MEMORY=0"}
|
||||
pm = _phase_memory()
|
||||
env = dict(environ or os.environ)
|
||||
event = pm.build_session_event(
|
||||
command=command,
|
||||
session_kind=pm.resolve_session_kind(env),
|
||||
outcome=outcome,
|
||||
need=need,
|
||||
route_id=route_id,
|
||||
agent_id=str(env.get("WARDEN_AGENT_ID") or ""),
|
||||
session_id=str(env.get("WARDEN_SESSION_ID") or ""),
|
||||
diagnostic_codes=diagnostic_codes,
|
||||
metadata=metadata,
|
||||
)
|
||||
return pm.record_session_event(pm.OpsWardenMemoryStore.open(environ=env), event)
|
||||
|
||||
|
||||
def worker_activation_context(need: str = "", environ: Mapping[str, str] | None = None) -> dict[str, Any]:
|
||||
env = dict(environ or os.environ)
|
||||
env["WARDEN_SESSION_KIND"] = "warden.worker"
|
||||
return activate(need=need, environ=env)
|
||||
|
||||
|
||||
def stabilized_route_for_need(need: str, environ: Mapping[str, str] | None = None) -> Optional[dict[str, Any]]:
|
||||
pm = _phase_memory()
|
||||
store = pm.OpsWardenMemoryStore.open(environ=environ)
|
||||
return pm.stabilized_route_match(store.list_events(), need=need)
|
||||
|
||||
|
||||
def format_activation_summary(activation: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
f"store: {activation.get('episode_count', 0)} episodes",
|
||||
f"session_kind: {activation.get('session_kind', '')}",
|
||||
f"selected: {len(activation.get('selected_episodes', ()) )}",
|
||||
]
|
||||
stabilized = activation.get("stabilized_route")
|
||||
if stabilized:
|
||||
lines.append(
|
||||
f"stabilized: {stabilized.get('route_id')} ({stabilized.get('confirmations')} confirmations)"
|
||||
)
|
||||
if activation.get("llm_calls_avoided"):
|
||||
lines.append("llm_calls_avoided: true")
|
||||
return "\n".join(lines)
|
||||
@@ -191,6 +191,7 @@ class LlmConnectBrain:
|
||||
def __init__(self, url: Optional[str] = None, timeout: float = 60.0):
|
||||
self.url = (url or os.environ.get("LLM_CONNECT_URL", DEFAULT_LLM_CONNECT_URL)).rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.memory_context: str = ""
|
||||
|
||||
def _call(self, prompt: str) -> str:
|
||||
resp = httpx.post(f"{self.url}/execute", json={"prompt": prompt}, timeout=self.timeout)
|
||||
@@ -203,9 +204,15 @@ class LlmConnectBrain:
|
||||
from_agent=str(message.get("from_agent", "")),
|
||||
subject=str(message.get("subject", "")),
|
||||
)
|
||||
prompt = (
|
||||
_CHARTER
|
||||
+ "\n--- MESSAGE (untrusted data) ---\n"
|
||||
prompt = _CHARTER
|
||||
if self.memory_context:
|
||||
prompt += (
|
||||
"\n--- ACTIVATED MEMORY (untrusted context) ---\n"
|
||||
+ self.memory_context
|
||||
+ "\n--- END ACTIVATED MEMORY ---\n"
|
||||
)
|
||||
prompt += (
|
||||
"\n--- MESSAGE (untrusted data) ---\n"
|
||||
+ f"from: {message.get('from_agent','')}\n"
|
||||
+ f"subject: {message.get('subject','')}\n"
|
||||
+ f"body: {message.get('body','')}\n"
|
||||
@@ -586,16 +593,88 @@ def draft_route_answer(query: str) -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _memory_activation_for_message(message: dict) -> tuple[Optional[dict], str]:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return None, ""
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return None, ""
|
||||
query = str(message.get("subject", "") or message.get("body", ""))
|
||||
try:
|
||||
activation = warden_memory.worker_activation_context(query)
|
||||
except RuntimeError:
|
||||
return None, ""
|
||||
from warden.memory import format_activation_summary
|
||||
|
||||
return activation, format_activation_summary(activation)
|
||||
|
||||
|
||||
def _plan_with_memory(message: dict, brain: Brain) -> WorkerPlan:
|
||||
activation, summary = _memory_activation_for_message(message)
|
||||
blob = f"{message.get('subject', '')} {message.get('body', '')}"
|
||||
if activation and activation.get("llm_calls_avoided") and _ROUTING_SIGNS.search(blob):
|
||||
wp = WorkerPlan(
|
||||
message_id=str(message.get("id", "")),
|
||||
from_agent=str(message.get("from_agent", "")),
|
||||
subject=str(message.get("subject", "")),
|
||||
)
|
||||
query = str(message.get("subject", "") or "")
|
||||
wp.actions.append(
|
||||
PlannedAction(
|
||||
kind="route_answer",
|
||||
summary="Answer from stabilized coordination memory.",
|
||||
payload={
|
||||
"query": query,
|
||||
"answer": draft_route_answer(query),
|
||||
"memory_stabilized": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
return wp
|
||||
if isinstance(brain, LlmConnectBrain) and summary:
|
||||
brain.memory_context = summary
|
||||
return brain.plan(message)
|
||||
|
||||
|
||||
def _record_worker_memory_outcome(plan: WorkerPlan) -> None:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return
|
||||
outcome = "escalated" if plan.escalated else "resolved"
|
||||
route_id = ""
|
||||
for action in plan.actions:
|
||||
if action.kind == "route_answer" and action.payload.get("memory_stabilized"):
|
||||
stabilized = warden_memory.stabilized_route_for_need(plan.subject)
|
||||
if stabilized:
|
||||
route_id = str(stabilized.get("route_id") or "")
|
||||
try:
|
||||
warden_memory.record_command_episode(
|
||||
command="worker run",
|
||||
outcome=outcome,
|
||||
need=plan.subject,
|
||||
route_id=route_id,
|
||||
diagnostic_codes=["worker_escalated"] if plan.escalated else [],
|
||||
metadata={"message_id": plan.message_id, "action_kinds": [a.kind for a in plan.actions]},
|
||||
)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
|
||||
def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]:
|
||||
"""Plan every message, attach computed route answers, and apply the guardrail pass."""
|
||||
plans: List[WorkerPlan] = []
|
||||
for m in messages:
|
||||
plan = brain.plan(m)
|
||||
plan = _plan_with_memory(m, brain)
|
||||
plan.raw = m
|
||||
for a in plan.actions:
|
||||
if a.kind == "route_answer" and "answer" not in a.payload:
|
||||
a.payload["answer"] = draft_route_answer(a.payload.get("query", m.get("subject", "")))
|
||||
plans.append(_guardrail(plan, m))
|
||||
_record_worker_memory_outcome(plans[-1])
|
||||
return plans
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user