Skip to content
Open
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ POWERCONTEXT_SERVER_RUNTIME_DREAM_MAX_PENDING_PER_SCOPE=32
# is absent and makes no model call.
# POWERCONTEXT_SERVER_RUNTIME_DECISION_ASSISTANCE_ENABLED=true

# Memory write gate: opt-in evidence-sufficiency check before a write commits. Disabled by
# default; when unset no gate runs and no extra model call is made.
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_ENABLED=true
# Direction only (which verdict means the cited evidence is insufficient): "yes" or "no".
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_HOLD_ON=yes
# Optional confidence floor below which a hold becomes a written-but-annotated change.
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_THRESHOLD=0.5

# Recall-sufficiency gate. Disabled by default; enabling expands thin recall up to two rounds.
# POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ENABLED=true
# POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_MAX_ROUNDS=2
Expand Down
11 changes: 11 additions & 0 deletions openapi/powercontext.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8194,6 +8194,17 @@ components:
memory:
$ref: "#/components/schemas/ArtifactReference"
nullable: true
held_count:
type: integer
minimum: 0
default: 0
description: Number of source windows held by the Memory write gate.
hold_codes:
type: array
items:
type: string
default: []
description: Structured Memory write gate refusal codes for held windows.
FlushTopicMemoryRequest:
type: object
additionalProperties: false
Expand Down
12 changes: 12 additions & 0 deletions src/powercontext/builtin/artifacts/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
MemoryEntryInactiveError,
MemoryEntryNotFoundError,
MemoryLayerError,
MemoryWriteRejectedError,
)
from powercontext.builtin.artifacts.memory.extraction import (
DefaultMemoryEvidenceProjector,
Expand Down Expand Up @@ -84,7 +85,12 @@
MemorySearchChannels,
MemorySearchRequest,
MemoryUnitOfWork,
MemoryWriteAssessment,
MemoryWriteGate,
MemoryWriteGateRequest,
MemoryWritePlan,
MemoryWriteRejectionCode,
MemoryWriteVerdict,
)
from powercontext.builtin.artifacts.memory.reranking import (
MEMORY_RERANK_INSTRUCTIONS,
Expand Down Expand Up @@ -170,7 +176,13 @@
"MemoryService",
"MemoryUnitOfWork",
"MemoryUsedSearchMode",
"MemoryWriteAssessment",
"MemoryWriteGate",
"MemoryWriteGateRequest",
"MemoryWritePlan",
"MemoryWriteRejectedError",
"MemoryWriteRejectionCode",
"MemoryWriteVerdict",
"memory_extraction_instructions",
"memory_extraction_instructions_version",
]
14 changes: 14 additions & 0 deletions src/powercontext/builtin/artifacts/memory/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,19 @@ def __init__(self, code: str) -> None:
super().__init__(messages.get(code, f"invalid memory citation: {code}"))


class MemoryWriteRejectedError(MemoryLayerError, RuntimeError):
"""A structured, caller-visible refusal to apply one Memory write.

``code`` and ``reason`` carry the gate's decision to the host, so a refused write is
observable rather than silently dropped.
"""

def __init__(self, code: str, reason: str | None = None) -> None:
self.code = code
self.reason = reason
detail = "" if reason is None else f": {reason}"
super().__init__(f"memory write was rejected ({code}){detail}")


class MemoryBackendConfigurationError(MemoryLayerError, RuntimeError):
"""Raised when a repository cannot satisfy its declared configuration."""
65 changes: 64 additions & 1 deletion src/powercontext/builtin/artifacts/memory/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
from __future__ import annotations

from contextlib import AbstractAsyncContextManager
from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

from powercontext.artifacts import Artifact, ArtifactRef
from powercontext.builtin.artifacts.memory.models import (
Expand Down Expand Up @@ -65,11 +67,72 @@ class MemoryCommit(BaseModel):
projections: tuple[MemoryProjection, ...]


class MemoryWriteRejectionCode(StrEnum):
"""Structured, caller-visible vocabulary for a held Memory write.

Names mirror the evidence-selection vocabulary so a host can branch on one stable set of
codes instead of parsing prose.
"""

NEEDS_EVIDENCE = "needs_evidence"
EVIDENCE_LIMIT_EXCEEDED = "evidence_limit_exceeded"
INSUFFICIENT_COVERAGE = "insufficient_coverage"


class MemoryWriteVerdict(StrEnum):
"""The complete verdict vocabulary a Memory write gate may produce."""

ACCEPT = "accept"
FLAG = "flag"
HOLD = "hold"


class MemoryWriteAssessment(BaseModel):
"""One gate verdict with the structured refusal a caller can observe.

``HOLD`` always carries both a ``code`` and a ``reason``: a refused write is visible to
its caller, never silently dropped. ``ACCEPT``/``FLAG`` leave ``code`` unset.
"""

model_config = ConfigDict(frozen=True)

verdict: MemoryWriteVerdict
policy_id: str
code: MemoryWriteRejectionCode | None = None
reason: str | None = None
used_fallback: bool = False


@dataclass(frozen=True, slots=True)
class MemoryWriteGateRequest:
"""A bounded projection of one pending Memory write for sufficiency judgement."""

candidates: tuple[str, ...]
evidence: tuple[str, ...]
expected_revision: int | None = None


class MemoryWriteGate(Protocol):
"""Judge whether a pending Memory write is supported by its cited evidence.

A gate only classifies: it never writes, approves, rejects, or deletes anything. A missing
or failing gate must be treated by callers as a pass-through, never as a hold.
"""

policy_id: str

async def assess(self, request: MemoryWriteGateRequest, /) -> MemoryWriteAssessment:
"""Return one verdict for a candidate set and its bounded evidence projection."""

...


class MemoryWritePlan(BaseModel):
"""A side-effect-free result that can be committed in an outer transaction."""

result: Memory | None
commit: MemoryCommit | None
decision: MemoryWriteAssessment | None = None


class MemorySearchRequest(BaseModel):
Expand Down
Loading
Loading