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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ e2e-test: ## Run CLI to Client SDK to Server end-to-end tests.
evaluation-unit-test: ## Run the evaluation project's unit tests, the work-continuity benchmark included.
@uv sync --project evaluation --frozen
@uv run --project evaluation pytest -c evaluation/pyproject.toml \
evaluation/artifact_evolution/tests \
evaluation/coding/swebench_pro/tests/unit \
evaluation/coding/work_continuity/tests/unit \
evaluation/memory/longmemeval_v2/tests -m "not live" -q
Expand Down
12 changes: 9 additions & 3 deletions evaluation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ Python environment and build configuration.
| [`memory/`](memory/README.md) | Memory retrieval, answer quality, and write-gate evidence | [LoCoMo](memory/locomo/README.md), [LoCoMo-Plus](memory/locomo_plus/README.md), [LongMemEval-V2](memory/longmemeval_v2/README.md), [Memory write gate](memory/write_gate/README.md) |
| [`performance/`](performance/README.md) | Load, latency, storage growth, and capacity limits | None yet |
| [`skills/`](skills/README.md) | Agent instruction routing and authorization regressions | [Claude Code skill-up](skills/skill-up/README.md), [ZCode native CLI](zcode_guidance/README.md) |
| [`artifact_evolution/`](artifact_evolution/README.md) | Evaluation-owned Dream recording and faithful replay | [Artifact evolution](artifact_evolution/README.md) |

```text
evaluation/
├── pyproject.toml # Shared powercontext-eval environment and build
├── uv.lock
├── artifact_evolution/ # Artifact-evolution recording and faithful replay
│ ├── src/powercontext_eval_artifact_evolution/
│ ├── tests/
│ └── README.md
├── coding/
│ ├── swebench_pro/
│ │ ├── src/powercontext_eval_swebench_pro/
Expand Down Expand Up @@ -89,9 +94,10 @@ uv run --directory evaluation ty check
uv build --project evaluation
```

The `powercontext-eval` distribution contains three packages: `powercontext_eval_swebench_pro`,
`powercontext_eval_work_continuity`, and `powercontext_eval_longmemeval_v2`. Editable installs and wheels expose the
independent `swebench-pro`, `work-continuity`, and `longmemeval-v2` commands and their suite modules.
The `powercontext-eval` distribution contains four packages: `powercontext_eval_artifact_evolution`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 同一份 README 内的 suite 清单不一致:新段落写四个命令,上面两处仍只列三个

本段现在声明 powercontext-eval 含四个包、四个独立命令,但同文件里另外两处没有同步,读者按 "Run a suite" 或 "Checks" 执行会漏掉新套件:

  • evaluation/README.md:55:仍是 "SWE-bench Pro, Work continuity, and LongMemEval-V2 share this directory's locked environment and expose independent ... commands",没有 artifact-evolution;
  • evaluation/README.md:61-63:--help 示例只有 swebench-pro / work-continuity / longmemeval-v2,缺 uv run --project evaluation artifact-evolution --help;
  • evaluation/README.md:91-92:文档化的检查命令 ruff check evaluation/coding evaluation/memory/longmemeval_v2 与 ruff format --check ... 也没有新目录,本 PR 新增的 evaluation/artifact_evolution/ruff.toml 因而不在任何被文档化的 lint 入口内(PR 正文里的 uv run ruff check artifact_evolution 是在该子目录内执行的,与本处命令的选择集不同)。

三处都是本 PR 同一份文件内的既有清单,建议一次性补齐,并把 artifact-evolution 的 lint 入口写清楚,避免下一位维护者只改其中一处。

`powercontext_eval_swebench_pro`, `powercontext_eval_work_continuity`, and `powercontext_eval_longmemeval_v2`.
Editable installs and wheels expose the independent `artifact-evolution`, `swebench-pro`, `work-continuity`, and
`longmemeval-v2` commands and their suite modules.
LoCoMo behavior tests remain under [`tests/`](../tests/), including [`tests/evaluation/`](../tests/evaluation/).
Skill-up and ZCode guidance keep their own validation tests. The [Bub replay harness](../e2e/bub/README.md) and
[product E2E tests](../tests/e2e/) cover cross-component acceptance.
Expand Down
60 changes: 60 additions & 0 deletions evaluation/artifact_evolution/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Artifact-evolution evaluation

This suite owns the evaluation-only recording and faithful replay contract for
Experience Dream evolution. It does not extend Runtime, add database tables, or
create Candidates. The bundle stores exact references, digests, realized
results, deterministic validation results, nomination state, and saved cost.

The versioned bundle is `powercontext.artifact-evolution/v1`. Controlled capture
is explicit; ordinary production capture is not enabled by this suite. Prompt,
task, and evidence bodies are not part of the contract. Optional output payloads
require either the deterministic `redacted` policy (sensitive keys and
credential-shaped strings are replaced before persistence) or an `approved`
policy with an explicit `approval_ref`. The recorder stores a structured
`scope_id`/`run_id` reference to the terminal Dream run; it does not copy the
Runtime row or create a Candidate.

## Commands

Validate a bundle without calling a model or writing Runtime state:

```bash
uv run --project evaluation artifact-evolution validate path/to/bundle.json
```

Replay one exact action using the saved result only:

```bash
uv run --project evaluation artifact-evolution replay path/to/bundle.json \
--decision-id decision-1 \
--trajectory-id trajectory-1 \
--observations-digest 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--action-signature refine-experience:v1 \
--ordering 0
```

Report exact replay coverage and saved generation cost:

```bash
uv run --project evaluation artifact-evolution coverage path/to/bundle.json
```

Record one controlled terminal `refine_experience` Dream snapshot into a new
bundle. The input is a JSON serialization of the existing Runtime `DreamRun`
shape; the evaluation package only reads the fields it needs and computes the
observation and output digests.

```bash
uv run --project evaluation artifact-evolution record \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 按本段命令记录 proposed 终态必然失败,且只回显原始 pydantic 错误

CapturePolicy.payloads 的默认值是 "none",而 record_dream_attempt 对 outcome == "proposed" 总会生成 output_payload(取 candidate_id / candidate_version)。ArtifactEvolutionBundle._validate_references 在 payloads == "none" 时拒绝任何 payload,因此用默认 capture policy 的 base bundle 执行本段命令,对最常见的 proposed 结果一定报错。

实测(隔离镜像 head 1787c1a2,probes/probe_record_cli.py,直接调用本仓库的 cli.app):

  • base bundle 省略 capture_policy.payloads(即默认 none)、Dream 快照为 status=succeeded, outcome=proposed → record 退出码 1,输出 Artifact-evolution record failed: 1 validation error for ArtifactEvolutionBundle / Value error, output_payload requires an approved or redacted capture policy,输出 bundle 未创建;
  • 同一输入把 policy 改成 payloads=redacted → 退出码 0,落盘 output_payload == {"candidate_id": "cand-1", "candidate_version": 2}。

本段把 record 写成可直接执行的命令,却没有说明 base bundle 必须先设 payloads=redacted|approved;文档前文"Optional output payloads require either ... or ..."对 proposed 也不成立——这里的 payload 不是可选的。建议:① 在 record 段写明 proposed 结果的 base bundle 必须使用 redacted 或 approved;② 视需要给 record 增加显式 policy 选项;③ CLI 把该错误归一化成领域错误文案,而不是透传 pydantic 的完整 input_value(当前错误串会把整个 bundle 结构回显到 stderr)。

base-bundle.json dream-run.json recorded-bundle.json \
--action-signature refine-experience:v1 \
--policy-id baseline-policy-v1 \
--generator-id dream.refine_experience \
--runtime-version runtime-v1
```

An absent or mismatched action returns `out_of_support`. Replay never runs a
generator or evaluator and never mutates Runtime, Candidate, Review, or Artifact
state. Exact replay support is identity-level: the trajectory, decision id,
observation digest, action signature, and ordering must all match. Coverage
therefore reports unsupported decision ids and is not a cross-policy comparison.
16 changes: 16 additions & 0 deletions evaluation/artifact_evolution/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

target-version = "py311"
line-length = 120
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Evaluation-owned recording and faithful replay for Artifact evolution."""

from powercontext_eval_artifact_evolution.bundle import (
BundleIOError,
BundleValidationError,
load_bundle,
record_attempt,
write_bundle,
)
from powercontext_eval_artifact_evolution.models import (
ArtifactEvolutionBundle,
DecisionRequest,
DreamRunRef,
ProposalAttempt,
)
from powercontext_eval_artifact_evolution.recording import (
DreamRecordingError,
DreamRunSnapshot,
decision_for_dream_run,
record_dream_attempt,
snapshot_from_dream_run,
)
from powercontext_eval_artifact_evolution.replay import (
ReplayEngine,
ReplayResult,
)

__all__ = [
"ArtifactEvolutionBundle",
"BundleIOError",
"BundleValidationError",
"DecisionRequest",
"DreamRecordingError",
"DreamRunRef",
"DreamRunSnapshot",
"ProposalAttempt",
"ReplayEngine",
"ReplayResult",
"decision_for_dream_run",
"load_bundle",
"record_attempt",
"record_dream_attempt",
"snapshot_from_dream_run",
"write_bundle",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Read, write, and append operations for artifact-evolution bundles."""

from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path

from pydantic import ValidationError

from powercontext_eval_artifact_evolution.models import ArtifactEvolutionBundle, ProposalAttempt
from powercontext_eval_artifact_evolution.privacy import digest_json, redact_payload


class BundleIOError(ValueError):
"""A bundle could not be read or written without losing its contract."""


class BundleValidationError(BundleIOError):
"""A bundle is not valid `powercontext.artifact-evolution/v1` JSON."""


def load_bundle(path: Path) -> ArtifactEvolutionBundle:
"""Load and validate a UTF-8 JSON bundle without invoking any evaluator."""

resolved = path.resolve()
try:
raw = resolved.read_bytes()
except OSError as error:
raise BundleIOError(f"cannot read artifact-evolution bundle: {resolved}") from error
if not raw.strip():
raise BundleValidationError(f"artifact-evolution bundle is blank: {resolved}")
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise BundleValidationError("artifact-evolution bundle must be UTF-8 JSON") from error
if not isinstance(value, dict):
raise BundleValidationError("artifact-evolution bundle must be a JSON object")
try:
return ArtifactEvolutionBundle.model_validate_json(raw)
except ValidationError as error:
raise BundleValidationError(f"invalid artifact-evolution bundle: {error}") from error


def write_bundle(bundle: ArtifactEvolutionBundle, path: Path) -> Path:
"""Write one bundle, refusing to overwrite a previously recorded artifact."""

target = path.resolve()
try:
bundle = ArtifactEvolutionBundle.model_validate(bundle.model_dump(mode="python", by_alias=True))
except ValidationError as error:
raise BundleValidationError(f"cannot write invalid artifact-evolution bundle: {error}") from error
temporary: Path | None = None
try:
target.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
temporary = Path(temporary_name)
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
stream.write(_render(bundle))
stream.flush()
os.fsync(stream.fileno())
# Hard-linking the completed file gives create-without-overwrite semantics.
os.link(temporary, target)
except FileExistsError as error:
raise BundleIOError(f"refusing to overwrite artifact-evolution bundle: {target}") from error
except OSError as error:
raise BundleIOError(f"cannot write artifact-evolution bundle: {target}") from error
finally:
if temporary is not None:
try:
temporary.unlink()
except FileNotFoundError:
pass
return target


def record_attempt(bundle: ArtifactEvolutionBundle, attempt: ProposalAttempt) -> ArtifactEvolutionBundle:
"""Return a validated bundle with one additional realized attempt.

This pure operation makes recording explicit and keeps the bundle immutable;
persistence remains the caller's choice through :func:`write_bundle`.
"""

if attempt.result.output_payload is not None:
payload = attempt.result.output_payload
if bundle.capture_policy.payloads == "redacted":
payload = redact_payload(payload)
attempt = attempt.model_copy(
update={
"result": attempt.result.model_copy(
update={"output_payload": payload, "output_digest": digest_json(payload)}
)
}
)
updated = bundle.model_copy(update={"attempts": (*bundle.attempts, attempt)})
return ArtifactEvolutionBundle.model_validate(updated.model_dump(mode="python", by_alias=True))


def _render(bundle: ArtifactEvolutionBundle) -> str:
payload = bundle.model_dump(mode="json", by_alias=True, exclude_none=True)
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
Loading
Loading