Skip to content
Merged
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
62 changes: 60 additions & 2 deletions docs/reference/reward-memory-decision-consumption.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ Read-authority checkpoints must match the exact consumer surface and corpus;
a turn-admission checkpoint cannot authorize a different review surface.
`freshness_context.age_seconds`, when supplied, is a nonnegative integer.
Rejected requests expose only the original hook's allowlisted
`boundary_reason_code`, never exception text or private input values.
`boundary_reason_code` and, for typed input errors, `boundary_detail_code`,
never exception text or private input values. The details distinguish
`freshness_age_invalid`, `freshness_context_invalid`,
`read_authority_checkpoint_missing` and `read_authority_checkpoint_invalid`.
Existing reason codes, ValueError compatibility, validation order and gates remain unchanged.

使用上述导出入口和 `resolve_reward_memory_experiment` 的原配置读回;不可用时
不能拿未经验证的配置替代。原 hook 的范围、revision、问题、时点、时效/冲突、
Expand All @@ -36,7 +40,44 @@ Rejected requests expose only the original hook's allowlisted

读授权 checkpoint 必须匹配本次 surface/corpus,不能拿 Turn 准入的 checkpoint
授权另一评审入口;age_seconds 如提供,须为非负整数。拒绝回执仅投影原 hook
白名单内的 boundary_reason_code,不暴露异常正文或私有参数。
白名单内的 boundary_reason_code,以及输入错误的 boundary_detail_code;细分年龄非法、
时效上下文非法、读授权缺失和读授权格式非法,不暴露异常正文或私有参数。
保留原错误码、ValueError 兼容、校验顺序与门禁。

Use `build_reward_memory_surface_read_authority_checkpoints(config, surface_id,
verified=original_proof_verified, source_ref=original_read_authority_source)`
from the same package. It selects only that surface's configured corpora through
the existing configuration owner, then TS assembles the exact workspace/project,
optional user/peer/session, read-authority and surface references. The caller must
actually verify its original read authority: an enabled config or ingest policy
alone is not read proof. `verified=False` stays false and blocks recall. It does
not infer a proof source, enable the capability or contact a provider. The Turn
wrapper uses this same projection and retains its verified registry source.

通用 helper 按实际 surface 和原配置选择 corpus,由 TS 组装精确范围;调用方仍须
真实核验原读权限并显式传入 verified/source_ref,不能把开关或写入 policy 当作读授权。
False 不会升级为 True;不推断授权来源、不启用能力、不调用 provider。原 Turn wrapper
复用该投影并保留 registry 来源。不要以生成了 checkpoint 为由宣称授权核验已完成。

Checkpoint transport failure remains optional-enrichment failure: managed Turn
admission returns its existing fail-open `runtime_unavailable` packet. The explicit
`agent-turn-recall --execute` CLI returns a safe `runtime_unavailable` packet and
exit code 2. Neither path calls the provider or writes a successful same-Turn
receipt when checkpoint construction fails; a later healthy retry uses the same
Turn identity. These zero-call guarantees apply before provider invocation only.

checkpoint 传输失败不成为普通 Turn 的新门禁:managed 准入沿用原 fail-open
`runtime_unavailable`;显式 CLI 返回安全的同类 packet 和退出码 2。构建失败时
均不调用 provider、不写成功的同 Turn 回执;恢复后沿用原 Turn 身份重试。
零调用保证仅适用于 provider 调用前的构建失败,不覆盖调用后的异常。

If computing age from timestamps, first reject an observation in the future;
then round elapsed seconds upward to an integer. Never clamp a negative age,
refresh the original observation time, or change policy to make recall pass.
This helper intentionally does not calculate or correct age for the caller.

由时间戳计算年龄时,先拒绝未来观察,再将经过秒数向上取整;不能截断负值、
刷新原观察时间或改 policy 来过门。helper 不替调用方计算或纠正年龄。

TypeScript owns admission and completion (`reward_memory.decision.plan/project`);
Python adapts the existing provider/applier and retains transient private values.
Expand All @@ -48,6 +89,14 @@ TS 负责准入和完成语义;Python 只适配现有 provider/applier 并保
TS 只收到引用、状态、计数与摘要,不收到问题、经验正文、原产物或模型判断内容;
不新增存储、SDK、密钥、开关或行动授权。

This slice does not migrate the existing Python SDK's scope/freshness validation;
it adds no second TS admission rule for those checks. Python remains the original
configuration/provider adapter and input-error source; TS owns the shared
checkpoint projection and allowlisted decision diagnostics.

此切片不迁移原 Python SDK 的范围/时效校验,也不在 TS 复制准入规则。Python 保留
原配置/provider 适配与输入错误来源,TS 持有共享 checkpoint 投影及白名单诊断。

| Mode / 模式 | Provider / 调用 | Meaning / 意义 |
| --- | --- | --- |
| Disabled/unconfigured / 未配置或关闭 | Zero; returns `None` / 零调用,无新 packet | Original path unchanged / 原路径不变 |
Expand Down Expand Up @@ -111,6 +160,12 @@ returns `replay_request_mismatch`. Reassessment uses retained qualified items an
the original **cumulative** multi-corpus counters, not a second query. This is
caller-retained replay, not automatic cross-process persistence or a new cache.

Retain the complete private result, not just context/public_packet/application
receipt. `assess_reward_memory_decision` needs the exact recall session and
attribution. A lost session after EOF/restart is incomplete, even if context was
delivered; do not re-query or fabricate semantic completion. There is currently
no supported cross-process restore API. Caller-owned persistence and a future
validated restore contract remain separate from this in-process replay API.
The private result retains the **original context-delivery receipt** separately
from the later semantic receipt. TypeScript revalidates its application, artifact,
surface and lesson attribution, so assessment (including an incomplete assessment)
Expand All @@ -124,6 +179,9 @@ cannot recreate that private lineage or upgrade historical receipts.
`previous_result` 仅复用配置和输入均匹配的请求,变化则拒绝复用。后续判断使用
原条目和累计多 corpus 遥测,不重复查询。这不是自动跨进程存储或新的缓存。

需保留完整私有 result,不能只存 context/public_packet/application receipt。
EOF/重启丢失 recall_session 时,交付过上下文也不能完成 assessment;不重查、不补造
语义完成。当前没有受支持的跨进程恢复 API,持久化与后续验证恢复合同是独立缺口。
私有结果分别保留原上下文交付回执和后续语义回执,TS 对应用、产物、surface 与
经验归因重新核验;评估成功或不完整均不抹掉此前已验证的交付。直接语义 callback
没有该回执时仍为 `context_delivery_verified=false`,语义判断与效果另行记录。
Expand Down
24 changes: 21 additions & 3 deletions loopx/capabilities/agent_turn_recall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,31 @@ def handle_agent_turn_recall_command(
"experiment": experiment_status,
}
else:
try:
read_checkpoints = _read_authority_checkpoints(config, args.goal_id)
except RuntimeError:
# This failure precedes the provider; do not fabricate a
# zero-call receipt for errors after recall has begun.
payload = {
"ok": False,
"schema_version": AGENT_TURN_RECALL_SCHEMA_VERSION,
"status": "runtime_unavailable",
"reason_code": "automatic_recall_runtime_failed",
"goal_id": args.goal_id,
"agent_id": args.agent_id,
"provider_call_count": 0,
"grants_new_action_authority": False,
"quota_spend_performed": False,
"external_writes_performed": False,
"suppress_external_sinks": True,
}
print_payload(payload, output_format(args), _render)
return 2
payload = run_agent_turn_recall(
config,
situation,
observed_at=datetime.now(timezone.utc).isoformat(),
read_authority_checkpoints=_read_authority_checkpoints(
config, args.goal_id
),
read_authority_checkpoints=read_checkpoints,
) | {
"goal_id": args.goal_id,
"agent_id": args.agent_id,
Expand Down
25 changes: 4 additions & 21 deletions loopx/capabilities/agent_turn_recall/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
resolve_reward_memory_experiment,
resolve_reward_memory_surface_config,
)
from ..reward_memory.read_authority import build_reward_memory_surface_read_authority_checkpoints
from .core import (
AGENT_TURN_RECALL_SCHEMA_VERSION,
AGENT_TURN_RECALL_SURFACE_ID,
Expand Down Expand Up @@ -149,28 +150,10 @@ def resolve_reward_memory_turn_session_ref(
def reward_memory_turn_read_authority_checkpoints(
config: Mapping[str, Any], goal_id: str
) -> dict[str, dict[str, Any]]:
route = resolve_reward_memory_surface_config(
config,
AGENT_TURN_RECALL_SURFACE_ID,
return build_reward_memory_surface_read_authority_checkpoints(
config, AGENT_TURN_RECALL_SURFACE_ID,
verified=True, source_ref=f"registry:{goal_id}:reward-memory",
)
checkpoints: dict[str, dict[str, Any]] = {}
for item in route["recall_corpora"]:
corpus = item["corpus"]
scope = corpus["scope"]
checkpoint = {
"verified": True,
"corpus_id": corpus["corpus_id"],
"workspace_ref": scope["workspace_ref"],
"project_ref": scope["project_ref"],
"surface_id": AGENT_TURN_RECALL_SURFACE_ID,
"read_authority": corpus["read_authority"],
"source_ref": f"registry:{goal_id}:reward-memory",
}
for field in ("user_ref", "peer_ref", "session_ref"):
if scope.get(field):
checkpoint[field] = scope[field]
checkpoints[corpus["corpus_id"]] = checkpoint
return checkpoints


def deduplicated_agent_turn_recall_payload(
Expand Down
2 changes: 2 additions & 0 deletions loopx/capabilities/reward_memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
run_reward_memory_automatic_ingest_hook,
run_reward_memory_automatic_recall_hook,
)
from .read_authority import build_reward_memory_surface_read_authority_checkpoints
from .outcome_lifecycle import (
reconcile_pending_turn_outcome_ingests,
reconcile_pending_turn_outcome_ingests_fail_open,
Expand All @@ -73,6 +74,7 @@
"RewardMemoryDecisionResult",
"assess_reward_memory_decision",
"run_reward_memory_decision",
"build_reward_memory_surface_read_authority_checkpoints",
"RewardMemoryFilteredRecallItem",
"RewardMemoryRecallItem",
"RewardMemoryRecallSession",
Expand Down
83 changes: 60 additions & 23 deletions loopx/capabilities/reward_memory/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, Literal, get_args

from ...control_plane.runtime.public_safety import public_safe_compact_text
from ..context_providers import build_context_provider
Expand Down Expand Up @@ -98,6 +98,22 @@ class _ActiveItemDecision:
]


RecallInputErrorCode = Literal[
"freshness_age_invalid", "freshness_context_invalid",
"read_authority_checkpoint_missing", "read_authority_checkpoint_invalid",
]


class RewardMemoryRecallInputError(ValueError):
"""An existing SDK input rejection with an allowlisted, non-content code."""

def __init__(self, reason_code: RecallInputErrorCode, message: str) -> None:
if reason_code not in get_args(RecallInputErrorCode):
raise ValueError("unsupported recall input error code")
super().__init__(message)
self.reason_code = reason_code


def _token(value: object, label: str) -> str:
result = str(value or "").strip()
if not TOKEN_RE.fullmatch(result):
Expand Down Expand Up @@ -243,22 +259,17 @@ def _authority_checkpoint(
raw: object, *, corpus: Mapping[str, Any], request: Mapping[str, Any]
) -> tuple[dict[str, Any], list[str]]:
if not isinstance(raw, Mapping):
raise ValueError("read_authority_checkpoint must be an object")
checkpoint = {
"verified": _boolean(raw, "verified"),
"corpus_id": _token(raw.get("corpus_id"), "checkpoint.corpus_id"),
"workspace_ref": _token(raw.get("workspace_ref"), "checkpoint.workspace_ref"),
"project_ref": _token(raw.get("project_ref"), "checkpoint.project_ref"),
"surface_id": _token(raw.get("surface_id"), "checkpoint.surface_id"),
"read_authority": _token(
raw.get("read_authority"), "checkpoint.read_authority"
),
"source_ref": _optional_token(raw.get("source_ref"), "checkpoint.source_ref"),
}
for field in IDENTITY_SCOPE_FIELDS:
expected_scope = corpus["scope"].get(field)
if expected_scope:
checkpoint[field] = _optional_token(raw.get(field), f"checkpoint.{field}")
raise RewardMemoryRecallInputError(
"read_authority_checkpoint_missing" if raw is None else "read_authority_checkpoint_invalid",
"read_authority_checkpoint must be an object",
)
try:
checkpoint = _normalize_authority_checkpoint(raw, corpus=corpus)
except ValueError as exc:
raise RewardMemoryRecallInputError(
"read_authority_checkpoint_missing" if not raw else "read_authority_checkpoint_invalid",
str(exc),
) from exc
reasons: list[str] = []
expected = {
"corpus_id": corpus["corpus_id"],
Expand All @@ -283,22 +294,48 @@ def _authority_checkpoint(
return checkpoint, reasons


def _normalize_authority_checkpoint(
raw: Mapping[str, Any], *, corpus: Mapping[str, Any],
) -> dict[str, Any]:
checkpoint = {
"verified": _boolean(raw, "verified"),
"corpus_id": _token(raw.get("corpus_id"), "checkpoint.corpus_id"),
"workspace_ref": _token(raw.get("workspace_ref"), "checkpoint.workspace_ref"),
"project_ref": _token(raw.get("project_ref"), "checkpoint.project_ref"),
"surface_id": _token(raw.get("surface_id"), "checkpoint.surface_id"),
"read_authority": _token(
raw.get("read_authority"), "checkpoint.read_authority"
),
"source_ref": _optional_token(raw.get("source_ref"), "checkpoint.source_ref"),
}
for field in IDENTITY_SCOPE_FIELDS:
expected_scope = corpus["scope"].get(field)
if expected_scope:
checkpoint[field] = _optional_token(raw.get(field), f"checkpoint.{field}")
return checkpoint


def _freshness_reasons(
corpus: Mapping[str, Any], freshness: Mapping[str, Any]
) -> list[str]:
reasons: list[str] = []
mode = corpus["freshness"]["mode"]
source_truth_current = _boolean(freshness, "source_truth_current")
source_revision = _optional_token(
freshness.get("source_revision"), "freshness_context.source_revision"
)
try:
source_truth_current = _boolean(freshness, "source_truth_current")
source_revision = _optional_token(
freshness.get("source_revision"), "freshness_context.source_revision"
)
except ValueError as exc:
raise RewardMemoryRecallInputError("freshness_context_invalid", str(exc)) from exc
age_seconds = freshness.get("age_seconds")
if age_seconds is not None and (
isinstance(age_seconds, bool)
or not isinstance(age_seconds, int)
or age_seconds < 0
):
raise ValueError("freshness_context.age_seconds must be a non-negative integer")
raise RewardMemoryRecallInputError(
"freshness_age_invalid", "freshness_context.age_seconds must be a non-negative integer",
)
if mode in {"source_truth_bound", "execution_bound"} and not source_truth_current:
reasons.append("source_truth_not_current")
if mode in {"revision_bound", "session_archive_bound"} and (
Expand Down Expand Up @@ -391,7 +428,7 @@ def build_reward_memory_recall_request(
):
raise ValueError(f"limit must be between 1 and {MAX_RESULTS}")
if not isinstance(request.get("freshness_context"), Mapping):
raise ValueError("freshness_context must be an object")
raise RewardMemoryRecallInputError("freshness_context_invalid", "freshness_context must be an object")
if _boolean(request, "raw_content_captured"):
raise ValueError("recall requests must not capture raw content")

Expand Down
1 change: 1 addition & 0 deletions loopx/capabilities/reward_memory/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def _recall_telemetry(hook: Mapping[str, Any]) -> dict[str, Any]:
"filtered_count": sum(item.get("filtered_item_count", 0) for item in attempts),
"recall_status": attempts[-1].get("status") if attempts else None,
"boundary_reason_code": hook.get("reason_code"),
"boundary_detail_code": hook.get("boundary_detail_code"),
}


Expand Down
28 changes: 28 additions & 0 deletions loopx/capabilities/reward_memory/read_authority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Original configuration IO adapter for the shared TS checkpoint projection."""
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, cast

from ...control_plane.effect_runtime import effect_runtime_result
from .experiment import resolve_reward_memory_surface_config


def build_reward_memory_surface_read_authority_checkpoints(
config: Mapping[str, Any], surface_id: str, *, verified: bool, source_ref: str,
) -> dict[str, dict[str, Any]]:
"""Project only the configured surface; the caller supplies existing read proof.

Enabled configuration is not proof. False remains false. This does not call
a provider, select a policy source, or verify/expand the caller's authority.
"""
route = resolve_reward_memory_surface_config(config, surface_id)
result = effect_runtime_result("reward_memory.read_authority.surface_checkpoints", {
"surface_id": surface_id, "verified": verified, "source_ref": source_ref,
"corpora": [{"corpus_id": item["corpus"]["corpus_id"],
"read_authority": item["corpus"]["read_authority"],
"scope": {key: item["corpus"]["scope"].get(key) for key in (
"workspace_ref", "project_ref", "user_ref", "peer_ref", "session_ref",
)}} for item in route["recall_corpora"]],
})
return cast(dict[str, dict[str, Any]], result["checkpoints"])
9 changes: 9 additions & 0 deletions loopx/capabilities/reward_memory/runtime_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .application import (
RewardMemoryApplier,
RewardMemoryRecallSession,
RewardMemoryRecallInputError,
apply_reward_memory_recall,
build_reward_memory_recall_request,
execute_reward_memory_recall,
Expand Down Expand Up @@ -168,6 +169,14 @@ def run_reward_memory_automatic_recall_hook(
provider_binding=corpus_route["provider_binding"],
provider=provider,
)
except RewardMemoryRecallInputError as exc:
return base | {
"status": "guard_rejected",
"reason_code": "exact_corpus_request_invalid",
"boundary_detail_code": exc.reason_code,
"recall_attempts": attempts,
"telemetry": telemetry,
}
except (KeyError, OSError, RuntimeError, TypeError, ValueError):
return base | {
"status": "guard_rejected",
Expand Down
Loading
Loading