feat(history): define exact bounded execution contexts
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Scalar contracts for bounded execution context capture."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TextEvidence:
|
||||
value: str | None
|
||||
provenance: str
|
||||
source: str
|
||||
reason: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromptSnapshot:
|
||||
raw: TextEvidence
|
||||
effective: TextEvidence
|
||||
prepend: TextEvidence
|
||||
append: TextEvidence
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecipeContext:
|
||||
anchor_id: str
|
||||
positive: PromptSnapshot | None
|
||||
negative: PromptSnapshot | None
|
||||
role_provenance: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContextBundle:
|
||||
contexts: tuple[RecipeContext, ...]
|
||||
state: str
|
||||
reason: str
|
||||
format_version: int = 1
|
||||
|
||||
|
||||
MAX_FIELD_BYTES = 64 * 1024
|
||||
MAX_CONTEXT_BYTES = 256 * 1024
|
||||
MAX_ATTEMPT_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
def text_size(value: str, limit: int = MAX_FIELD_BYTES) -> int:
|
||||
"""Reject oversize strings before allocating encoded copies."""
|
||||
if type(value) is not str or len(value) > limit:
|
||||
raise ValueError("Invalid or oversized text")
|
||||
size = 0
|
||||
for char in value:
|
||||
code = ord(char)
|
||||
if 0xD800 <= code <= 0xDFFF:
|
||||
raise ValueError("Invalid Unicode")
|
||||
size += 1 if code < 128 else 2 if code < 2048 else 3 if code < 65536 else 4
|
||||
if size > limit:
|
||||
raise ValueError("Oversized text")
|
||||
return size
|
||||
|
||||
|
||||
def canonical_content(kind: str, fields: tuple[str | None, ...]) -> bytes:
|
||||
text_size(kind, 128)
|
||||
if type(fields) is not tuple:
|
||||
raise ValueError("Invalid canonical fields")
|
||||
for value in fields:
|
||||
if value is not None:
|
||||
text_size(value)
|
||||
return json.dumps(
|
||||
[1, kind, *fields], ensure_ascii=False, allow_nan=False, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def content_digest(kind: str, fields: tuple[str | None, ...]) -> str:
|
||||
return hashlib.sha256(canonical_content(kind, fields)).hexdigest()
|
||||
|
||||
|
||||
def snapshot_digest(snapshot: PromptSnapshot) -> str:
|
||||
return content_digest(
|
||||
"snapshot",
|
||||
tuple(
|
||||
getattr(snapshot, field).value
|
||||
for field in ("raw", "effective", "prepend", "append")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def context_digest(context: RecipeContext) -> str:
|
||||
return content_digest(
|
||||
"context",
|
||||
tuple(
|
||||
snapshot_digest(side) if side is not None else None
|
||||
for side in (context.positive, context.negative)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def validate_bundle(bundle: ContextBundle) -> None:
|
||||
if (
|
||||
type(bundle) is not ContextBundle
|
||||
or type(bundle.format_version) is not int
|
||||
or bundle.format_version != 1
|
||||
or type(bundle.contexts) is not tuple
|
||||
or len(bundle.contexts) > 16
|
||||
):
|
||||
raise ValueError("Invalid context bundle")
|
||||
total = text_size(bundle.state, 128) + text_size(bundle.reason, 128)
|
||||
anchors = set()
|
||||
for context in bundle.contexts:
|
||||
if type(context) is not RecipeContext:
|
||||
raise ValueError("Invalid recipe context")
|
||||
size = text_size(context.anchor_id, 512)
|
||||
if len(context.anchor_id) > 128:
|
||||
raise ValueError("Oversized recipe anchor")
|
||||
if not context.anchor_id or context.anchor_id in anchors:
|
||||
raise ValueError("Duplicate or empty recipe anchor")
|
||||
anchors.add(context.anchor_id)
|
||||
size += text_size(context.role_provenance, 128)
|
||||
for snapshot in (context.positive, context.negative):
|
||||
if snapshot is None:
|
||||
continue
|
||||
if type(snapshot) is not PromptSnapshot:
|
||||
raise ValueError("Invalid prompt snapshot")
|
||||
for field in (
|
||||
snapshot.raw,
|
||||
snapshot.effective,
|
||||
snapshot.prepend,
|
||||
snapshot.append,
|
||||
):
|
||||
if type(field) is not TextEvidence:
|
||||
raise ValueError("Invalid text evidence")
|
||||
if field.value is not None:
|
||||
size += text_size(field.value)
|
||||
size += text_size(field.provenance, 128) + text_size(field.source, 128)
|
||||
if field.reason is not None:
|
||||
size += text_size(field.reason, 128)
|
||||
if size > MAX_CONTEXT_BYTES:
|
||||
raise ValueError("Context payload limit reached")
|
||||
total += size
|
||||
if total > MAX_ATTEMPT_BYTES:
|
||||
raise ValueError("Attempt payload limit reached")
|
||||
Reference in New Issue
Block a user