open-reflection-protocol 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_reflection_protocol-0.3.0.dist-info/METADATA +262 -0
- open_reflection_protocol-0.3.0.dist-info/RECORD +29 -0
- open_reflection_protocol-0.3.0.dist-info/WHEEL +4 -0
- open_reflection_protocol-0.3.0.dist-info/entry_points.txt +2 -0
- orp/__init__.py +66 -0
- orp/adapters/__init__.py +6 -0
- orp/adapters/generic_json.py +24 -0
- orp/adapters/langgraph.py +24 -0
- orp/adapters/openai_agents.py +27 -0
- orp/adapters/otel.py +52 -0
- orp/capture.py +162 -0
- orp/cli.py +366 -0
- orp/compiler.py +124 -0
- orp/conflicts.py +62 -0
- orp/delivery.py +110 -0
- orp/effects.py +112 -0
- orp/evidence.py +92 -0
- orp/examples/failing_coding_agent.py +38 -0
- orp/experience.py +114 -0
- orp/export.py +60 -0
- orp/lessons.py +95 -0
- orp/mcp_server.py +171 -0
- orp/reflect.py +97 -0
- orp/replay.py +108 -0
- orp/rollback.py +82 -0
- orp/schema.py +303 -0
- orp/storage.py +459 -0
- orp/training.py +94 -0
- orp/viewer.py +104 -0
orp/reflect.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Reflection Analyzer — 诊断、替代策略、Challenger"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from orp.schema import (
|
|
6
|
+
ExperienceRecord, TimelineEvent, EventKind, ReflectionAnalysis,
|
|
7
|
+
)
|
|
8
|
+
from orp.storage import ORPStorage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReflectionAnalyzer:
|
|
12
|
+
"""反思分析 — 输出结构化候选,不直接修改 Agent"""
|
|
13
|
+
|
|
14
|
+
def analyze(self, record: ExperienceRecord) -> ReflectionAnalysis:
|
|
15
|
+
"""对 ExperienceRecord 执行反思分析"""
|
|
16
|
+
diagnosis = self._diagnose(record)
|
|
17
|
+
alternatives = self._suggest_alternatives(record)
|
|
18
|
+
limitations = self._find_limitations(record)
|
|
19
|
+
return ReflectionAnalysis(
|
|
20
|
+
diagnosis=diagnosis,
|
|
21
|
+
alternatives=alternatives,
|
|
22
|
+
limitations=limitations,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _diagnose(self, record: ExperienceRecord) -> Optional[str]:
|
|
26
|
+
"""从失败的运行中生成诊断"""
|
|
27
|
+
outcome_events = [
|
|
28
|
+
e for e in record.timeline
|
|
29
|
+
if e.kind == EventKind.OUTCOME
|
|
30
|
+
]
|
|
31
|
+
error_events = [
|
|
32
|
+
e for e in record.timeline
|
|
33
|
+
if e.kind == EventKind.OBSERVATION
|
|
34
|
+
and any(w in e.content.lower() for w in ["error", "fail", "exception", "traceback"])
|
|
35
|
+
]
|
|
36
|
+
if outcome_events:
|
|
37
|
+
return f"Outcome: {outcome_events[-1].content}"
|
|
38
|
+
if error_events:
|
|
39
|
+
return f"Detected error: {error_events[-1].content[:200]}"
|
|
40
|
+
if record.outcome.status == "failed":
|
|
41
|
+
return "Task failed — review timeline for root cause"
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def _suggest_alternatives(self, record: ExperienceRecord) -> list[str]:
|
|
45
|
+
"""基于失败的运行提出替代策略"""
|
|
46
|
+
suggestions = []
|
|
47
|
+
has_test = any(
|
|
48
|
+
"test" in e.content.lower() or "pytest" in e.content.lower()
|
|
49
|
+
for e in record.timeline
|
|
50
|
+
)
|
|
51
|
+
has_diff = any(
|
|
52
|
+
"git diff" in e.content.lower() or "diff" in e.content.lower()
|
|
53
|
+
for e in record.timeline
|
|
54
|
+
)
|
|
55
|
+
if record.outcome.status == "failed":
|
|
56
|
+
if not has_test:
|
|
57
|
+
suggestions.append("Run tests first to confirm the failure")
|
|
58
|
+
if not has_diff:
|
|
59
|
+
suggestions.append("Check git diff to understand what changed")
|
|
60
|
+
return suggestions
|
|
61
|
+
|
|
62
|
+
def _find_limitations(self, record: ExperienceRecord) -> list[str]:
|
|
63
|
+
"""识别这次运行的局限性"""
|
|
64
|
+
limits = []
|
|
65
|
+
if not record.task.get("input_ref"):
|
|
66
|
+
limits.append("No input reference recorded — cannot reproduce exact input")
|
|
67
|
+
claim_count = sum(1 for e in record.timeline if e.kind == EventKind.CLAIM)
|
|
68
|
+
evidence_count = sum(len(e.evidence_refs) for e in record.timeline)
|
|
69
|
+
if claim_count > evidence_count:
|
|
70
|
+
limits.append(f"More claims ({claim_count}) than evidence refs ({evidence_count})")
|
|
71
|
+
return limits
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Challenger:
|
|
75
|
+
"""Challenger — 质疑未经证明的声明
|
|
76
|
+
|
|
77
|
+
自动查找 ExperienceRecord 中的 claim 及其证据支持情况。
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def challenge(self, record: ExperienceRecord) -> list[dict[str, Any]]:
|
|
81
|
+
"""找出所有未经充分支持的声明"""
|
|
82
|
+
challenged: list[dict[str, Any]] = []
|
|
83
|
+
for evt in record.timeline:
|
|
84
|
+
if evt.kind == EventKind.CLAIM:
|
|
85
|
+
if not evt.evidence_refs:
|
|
86
|
+
challenged.append({
|
|
87
|
+
"event_id": evt.id,
|
|
88
|
+
"content": evt.content[:100],
|
|
89
|
+
"issue": "No evidence references provided",
|
|
90
|
+
})
|
|
91
|
+
elif len(evt.evidence_refs) < 2:
|
|
92
|
+
challenged.append({
|
|
93
|
+
"event_id": evt.id,
|
|
94
|
+
"content": evt.content[:100],
|
|
95
|
+
"issue": "Only 1 evidence ref — may be insufficient",
|
|
96
|
+
})
|
|
97
|
+
return challenged
|
orp/replay.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Counterfactual Replay — 隔离环境回放替代策略"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from orp.schema import CounterfactualReplay
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CounterfactualReplayer:
|
|
12
|
+
"""反事实回放 — 在隔离环境中比较原始策略与替代策略"""
|
|
13
|
+
|
|
14
|
+
def replay(self, experience_id: str, original: str, alternative: str,
|
|
15
|
+
workdir: Optional[str] = None) -> CounterfactualReplay:
|
|
16
|
+
"""尝试在隔离环境中回放替代策略
|
|
17
|
+
|
|
18
|
+
返回 CounterfactualReplay,其中 result.status 为:
|
|
19
|
+
- improved: 替代策略结果更好
|
|
20
|
+
- equivalent: 结果相当
|
|
21
|
+
- worse: 替代策略更差
|
|
22
|
+
- predicted: 无法实际回放,只能输出预测
|
|
23
|
+
"""
|
|
24
|
+
isolation = self._create_isolation(workdir)
|
|
25
|
+
if not isolation:
|
|
26
|
+
# 无法创建隔离环境,只能输出预测
|
|
27
|
+
return CounterfactualReplay(
|
|
28
|
+
experience_id=experience_id,
|
|
29
|
+
original_strategy=original,
|
|
30
|
+
alternative_strategy=alternative,
|
|
31
|
+
verification_mode="predicted",
|
|
32
|
+
result={"status": "predicted", "note": "Could not create isolation environment"},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# 运行替代策略
|
|
37
|
+
start_cmd = alternative.split()
|
|
38
|
+
if not start_cmd:
|
|
39
|
+
return CounterfactualReplay(
|
|
40
|
+
experience_id=experience_id,
|
|
41
|
+
original_strategy=original,
|
|
42
|
+
alternative_strategy=alternative,
|
|
43
|
+
verification_mode="sandbox_replay",
|
|
44
|
+
result={"status": "predicted", "note": "Empty alternative strategy"},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
start_cmd,
|
|
49
|
+
capture_output=True, text=True,
|
|
50
|
+
cwd=isolation, timeout=120,
|
|
51
|
+
)
|
|
52
|
+
success = result.returncode == 0
|
|
53
|
+
|
|
54
|
+
return CounterfactualReplay(
|
|
55
|
+
experience_id=experience_id,
|
|
56
|
+
original_strategy=original,
|
|
57
|
+
alternative_strategy=alternative,
|
|
58
|
+
verification_mode="sandbox_replay",
|
|
59
|
+
result={
|
|
60
|
+
"status": "improved" if success else "worse",
|
|
61
|
+
"exit_code": result.returncode,
|
|
62
|
+
"duration": "completed",
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
except subprocess.TimeoutExpired:
|
|
66
|
+
return CounterfactualReplay(
|
|
67
|
+
experience_id=experience_id,
|
|
68
|
+
original_strategy=original,
|
|
69
|
+
alternative_strategy=alternative,
|
|
70
|
+
verification_mode="sandbox_replay",
|
|
71
|
+
result={"status": "worse", "error": "timed out"},
|
|
72
|
+
)
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
return CounterfactualReplay(
|
|
75
|
+
experience_id=experience_id,
|
|
76
|
+
original_strategy=original,
|
|
77
|
+
alternative_strategy=alternative,
|
|
78
|
+
verification_mode="predicted",
|
|
79
|
+
result={"status": "predicted", "error": "command not found"},
|
|
80
|
+
)
|
|
81
|
+
finally:
|
|
82
|
+
self._cleanup_isolation(isolation)
|
|
83
|
+
|
|
84
|
+
def _create_isolation(self, workdir: Optional[str] = None) -> Optional[str]:
|
|
85
|
+
try:
|
|
86
|
+
tmp = tempfile.mkdtemp(prefix="orp_replay_")
|
|
87
|
+
if workdir and os.path.isdir(workdir):
|
|
88
|
+
# 复制工作目录内容(浅层)
|
|
89
|
+
for item in os.listdir(workdir):
|
|
90
|
+
src = os.path.join(workdir, item)
|
|
91
|
+
dst = os.path.join(tmp, item)
|
|
92
|
+
if os.path.isfile(src):
|
|
93
|
+
try:
|
|
94
|
+
with open(src, 'rb') as fsrc:
|
|
95
|
+
with open(dst, 'wb') as fdst:
|
|
96
|
+
fdst.write(fsrc.read())
|
|
97
|
+
except (PermissionError, OSError):
|
|
98
|
+
pass
|
|
99
|
+
return tmp
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _cleanup_isolation(self, path: str) -> None:
|
|
104
|
+
try:
|
|
105
|
+
import shutil
|
|
106
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
orp/rollback.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Rollback Manager — Lesson 降级、撤回与恢复"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from orp.schema import (
|
|
6
|
+
Lesson, LessonRollback, LessonStatus,
|
|
7
|
+
)
|
|
8
|
+
from orp.storage import ORPStorage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RollbackManager:
|
|
12
|
+
"""回滚管理 — 坏 Lesson 的审计撤回"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, storage: Optional[ORPStorage] = None):
|
|
15
|
+
self._storage = storage or ORPStorage()
|
|
16
|
+
|
|
17
|
+
def rollback(self, lesson_id: str, reason: str,
|
|
18
|
+
new_status: LessonStatus = LessonStatus.UNDER_REVIEW,
|
|
19
|
+
replacement_id: Optional[str] = None) -> Optional[LessonRollback]:
|
|
20
|
+
"""撤回一条 Lesson
|
|
21
|
+
|
|
22
|
+
默认进入 under_review 而非直接 rejected,保留复审机会。
|
|
23
|
+
"""
|
|
24
|
+
lesson = self._storage.get_lesson(lesson_id)
|
|
25
|
+
if not lesson:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
previous = lesson.status
|
|
29
|
+
rollback = LessonRollback(
|
|
30
|
+
lesson_id=lesson_id,
|
|
31
|
+
reason=reason,
|
|
32
|
+
previous_status=previous,
|
|
33
|
+
new_status=new_status,
|
|
34
|
+
replacement_lesson_id=replacement_id,
|
|
35
|
+
affected_deliveries=[
|
|
36
|
+
d.delivery_id
|
|
37
|
+
for d in self._storage.get_deliveries_for_lesson(lesson_id)
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 更新 Lesson 状态
|
|
42
|
+
self._storage.update_lesson_status(lesson_id, new_status)
|
|
43
|
+
# 保存回滚记录
|
|
44
|
+
self._storage.save_rollback(rollback)
|
|
45
|
+
|
|
46
|
+
# 如果是 POLICY_FILE 交付的,尝试从 AGENTS.md 移除
|
|
47
|
+
if previous == LessonStatus.ACTIVE:
|
|
48
|
+
self._cleanup_policy_file(lesson_id)
|
|
49
|
+
|
|
50
|
+
return rollback
|
|
51
|
+
|
|
52
|
+
def restore(self, lesson_id: str) -> bool:
|
|
53
|
+
"""将 under_review 的 Lesson 恢复到 active"""
|
|
54
|
+
lesson = self._storage.get_lesson(lesson_id)
|
|
55
|
+
if not lesson or lesson.status != LessonStatus.UNDER_REVIEW:
|
|
56
|
+
return False
|
|
57
|
+
self._storage.update_lesson_status(lesson_id, LessonStatus.ACTIVE)
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def _cleanup_policy_file(self, lesson_id: str) -> None:
|
|
61
|
+
"""从 AGENTS.md 中移除指定 Lesson 相关的区块"""
|
|
62
|
+
import os
|
|
63
|
+
try:
|
|
64
|
+
agents_path = os.path.join(os.getcwd(), "AGENTS.md")
|
|
65
|
+
if not os.path.exists(agents_path):
|
|
66
|
+
return
|
|
67
|
+
with open(agents_path, "r") as f:
|
|
68
|
+
content = f.read()
|
|
69
|
+
start_marker = f"<!-- ORP Lesson: {lesson_id} -->"
|
|
70
|
+
end_marker = "<!-- END ORP Lesson -->"
|
|
71
|
+
start = content.find(start_marker)
|
|
72
|
+
if start == -1:
|
|
73
|
+
return
|
|
74
|
+
end = content.find(end_marker, start)
|
|
75
|
+
if end == -1:
|
|
76
|
+
return
|
|
77
|
+
end += len(end_marker)
|
|
78
|
+
new_content = content[:start] + content[end:]
|
|
79
|
+
with open(agents_path, "w") as f:
|
|
80
|
+
f.write(new_content)
|
|
81
|
+
except (IOError, PermissionError, FileNotFoundError):
|
|
82
|
+
pass
|
orp/schema.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# ORP Core Schema v0.3
|
|
2
|
+
# 代码即规范 — 此文件中的所有 Pydantic 模型构成 ORP 协议的官方定义
|
|
3
|
+
#
|
|
4
|
+
# 设计原则:
|
|
5
|
+
# 1. Evidence first: 结论必须引用证据,无证据的标记为 claim
|
|
6
|
+
# 2. 区分事实与声明: observation/action 是事实,claim/decision 是声明
|
|
7
|
+
# 3. 可执行: 反思优先编译为 Lesson/Eval/Guardrail
|
|
8
|
+
# 4. Outcome based: 经验价值由后续任务结果决定
|
|
9
|
+
# 5. 基于 OpenTelemetry: 不替代 tracing,而是扩展它
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
from uuid import UUID, uuid4
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─── Helpers ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
def _now() -> datetime:
|
|
22
|
+
return datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
def _uuid() -> UUID:
|
|
25
|
+
return uuid4()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ─── Enums ───────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
class EventKind(str, Enum):
|
|
31
|
+
"""TimelineEvent 类型 — 必须区分可观察事实与 Agent 声明"""
|
|
32
|
+
OBSERVATION = "observation" # 工具/环境/外部系统产生的可观察结果
|
|
33
|
+
ACTION = "action" # Agent 或用户执行的动作
|
|
34
|
+
CLAIM = "claim" # Agent 对原因/状态/结果的声明
|
|
35
|
+
DECISION = "decision" # Agent 在多个方案间做出的选择
|
|
36
|
+
FEEDBACK = "feedback" # 人工/规则/模型/用户评价
|
|
37
|
+
OUTCOME = "outcome" # 测试/验收/生产指标等结果
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TrustLevel(str, Enum):
|
|
41
|
+
"""可信等级 — 不使用缺乏校准的单一评分"""
|
|
42
|
+
ASSERTED = "asserted" # 未经外部证据支持的声明
|
|
43
|
+
OBSERVED = "observed" # 被工具/环境/trace 观察到
|
|
44
|
+
REPRODUCED = "reproduced" # 独立重跑中复现
|
|
45
|
+
EXTERNALLY_VERIFIED = "externally_verified" # 被规则/测试/系统验证
|
|
46
|
+
HUMAN_CONFIRMED = "human_confirmed" # 被授权人工确认
|
|
47
|
+
REGRESSION_GUARDED = "regression_guarded" # 已形成持续运行的回归 Eval
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LessonStatus(str, Enum):
|
|
51
|
+
CANDIDATE = "candidate" # 由单次经验生成,尚未验证
|
|
52
|
+
ACTIVE = "active" # 通过外部验证,可被检索
|
|
53
|
+
UNDER_REVIEW = "under_review" # 发现冲突/负面效果,暂停默认交付
|
|
54
|
+
DEPRECATED = "deprecated" # 效果不佳/冲突/过期
|
|
55
|
+
REJECTED = "rejected" # 被证明错误
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DeliveryStrategy(str, Enum):
|
|
59
|
+
MCP_TOOL = "mcp_tool" # Agent 主动调用 MCP 工具
|
|
60
|
+
PROMPT_CONTEXT = "prompt_context" # 运行时注入系统/任务上下文
|
|
61
|
+
POLICY_FILE = "policy_file" # 写入 AGENTS.md 等策略文件
|
|
62
|
+
RUNTIME_HOOK = "runtime_hook" # 高风险动作前条件式注入
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FeedbackSourceType(str, Enum):
|
|
66
|
+
HUMAN = "human"
|
|
67
|
+
DETERMINISTIC = "deterministic"
|
|
68
|
+
LLM_JUDGE = "llm_judge"
|
|
69
|
+
USER = "user"
|
|
70
|
+
PRODUCTION_METRIC = "production_metric"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EvaluationMethod(str, Enum):
|
|
74
|
+
DESCRIPTIVE = "descriptive" # 仅记录,不声称因果
|
|
75
|
+
MATCHED_BASELINE = "matched_baseline" # 与相似任务基线比较
|
|
76
|
+
RANDOMIZED = "randomized" # A/B 实验
|
|
77
|
+
CAUSAL_MODEL = "causal_model" # 贝叶斯分层等因果方法
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TrainingFormat(str, Enum):
|
|
81
|
+
SFT_EXAMPLE = "sft_example"
|
|
82
|
+
PREFERENCE_PAIR = "preference_pair"
|
|
83
|
+
CRITIQUE_REVISION = "critique_revision"
|
|
84
|
+
NEGATIVE_EXAMPLE = "negative_example"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TrainingStatus(str, Enum):
|
|
88
|
+
CANDIDATE = "candidate"
|
|
89
|
+
APPROVED = "approved"
|
|
90
|
+
REJECTED = "rejected"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ─── Core Objects ────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
class EvidenceRef(BaseModel):
|
|
96
|
+
"""证据引用 — 必须可定位、可校验"""
|
|
97
|
+
evidence_id: str
|
|
98
|
+
kind: str = Field(default="tool_output")
|
|
99
|
+
uri: Optional[str] = None
|
|
100
|
+
digest: Optional[str] = None
|
|
101
|
+
created_at: datetime = Field(default_factory=_now)
|
|
102
|
+
redaction: Optional[dict[str, Any]] = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Feedback(BaseModel):
|
|
106
|
+
"""外部评价 — 必须记录来源"""
|
|
107
|
+
target_ref: str
|
|
108
|
+
source_type: FeedbackSourceType
|
|
109
|
+
source_id: str
|
|
110
|
+
verdict: str
|
|
111
|
+
explanation: Optional[str] = None
|
|
112
|
+
evidence_refs: list[str] = Field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TimelineEvent(BaseModel):
|
|
116
|
+
"""时间线事件 — 推理/操作/观察序列中的一个原子项"""
|
|
117
|
+
id: str = Field(default_factory=lambda: f"evt_{uuid4().hex[:8]}")
|
|
118
|
+
kind: EventKind
|
|
119
|
+
source: str = Field(default="agent") # agent | tool | human | system
|
|
120
|
+
content: str
|
|
121
|
+
evidence_refs: list[str] = Field(default_factory=list)
|
|
122
|
+
parent_event: Optional[str] = None
|
|
123
|
+
timestamp: datetime = Field(default_factory=_now)
|
|
124
|
+
|
|
125
|
+
@field_validator("content")
|
|
126
|
+
@classmethod
|
|
127
|
+
def content_not_empty(cls, v: str) -> str:
|
|
128
|
+
if not v.strip():
|
|
129
|
+
raise ValueError("Event content cannot be empty")
|
|
130
|
+
return v
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Outcome(BaseModel):
|
|
134
|
+
"""运行结果 — 基于客观信号"""
|
|
135
|
+
status: str = Field(default="unknown") # success | failed | partial | unknown
|
|
136
|
+
objective_signals: list[dict[str, Any]] = Field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ReflectionAnalysis(BaseModel):
|
|
140
|
+
"""反思分析 — Agent 或 Challenger 对运行的结构化复盘"""
|
|
141
|
+
diagnosis: Optional[str] = None
|
|
142
|
+
alternatives: list[str] = Field(default_factory=list)
|
|
143
|
+
limitations: list[str] = Field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ExperienceRecord(BaseModel):
|
|
147
|
+
"""经验记录 — 一次 Agent 运行及其复盘结果"""
|
|
148
|
+
orp_version: str = Field(default="0.3")
|
|
149
|
+
experience_id: str = Field(default_factory=lambda: f"exp_{uuid4().hex[:12]}")
|
|
150
|
+
trace_ref: Optional[str] = None
|
|
151
|
+
agent: dict[str, Any] = Field(default_factory=lambda: {"id": "unknown", "version": "", "model": ""})
|
|
152
|
+
task: dict[str, Any] = Field(default_factory=lambda: {"goal": "", "domain": "", "input_ref": ""})
|
|
153
|
+
timeline: list[TimelineEvent] = Field(default_factory=list)
|
|
154
|
+
outcome: Outcome = Field(default_factory=Outcome)
|
|
155
|
+
reflection: Optional[ReflectionAnalysis] = None
|
|
156
|
+
artifacts: dict[str, list[str]] = Field(default_factory=lambda: {"lessons": [], "evals": [], "guardrails": []})
|
|
157
|
+
feedback: list[Feedback] = Field(default_factory=list)
|
|
158
|
+
created_at: datetime = Field(default_factory=_now)
|
|
159
|
+
|
|
160
|
+
@field_validator("timeline")
|
|
161
|
+
@classmethod
|
|
162
|
+
def timeline_not_empty(cls, v: list[TimelineEvent]) -> list[TimelineEvent]:
|
|
163
|
+
if not v:
|
|
164
|
+
raise ValueError("Timeline must have at least one event")
|
|
165
|
+
return v
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Lesson(BaseModel):
|
|
169
|
+
"""课程/经验 — 可在未来任务中检索的条件化经验"""
|
|
170
|
+
lesson_id: str = Field(default_factory=lambda: f"lesson_{uuid4().hex[:12]}")
|
|
171
|
+
trigger: dict[str, Any] = Field(default_factory=lambda: {"domain": "", "conditions": []})
|
|
172
|
+
recommendation: str
|
|
173
|
+
provenance: dict[str, Any] = Field(default_factory=lambda: {"experience_ids": [], "evals": []})
|
|
174
|
+
scope: dict[str, Any] = Field(default_factory=lambda: {
|
|
175
|
+
"task_domains": [], "frameworks": [], "agent_versions": []
|
|
176
|
+
})
|
|
177
|
+
relationships: dict[str, list[str]] = Field(default_factory=lambda: {
|
|
178
|
+
"conflicts_with": [], "supersedes": [], "superseded_by": []
|
|
179
|
+
})
|
|
180
|
+
validation: dict[str, Any] = Field(default_factory=lambda: {"level": "asserted", "evidence_refs": []})
|
|
181
|
+
metrics: dict[str, Any] = Field(default_factory=lambda: {
|
|
182
|
+
"retrieved": 0, "delivered": 0, "acknowledged": 0, "applied": 0,
|
|
183
|
+
"successful_after_apply": 0, "estimated_effect": None
|
|
184
|
+
})
|
|
185
|
+
status: LessonStatus = LessonStatus.CANDIDATE
|
|
186
|
+
expires_at: Optional[datetime] = None
|
|
187
|
+
created_at: datetime = Field(default_factory=_now)
|
|
188
|
+
updated_at: datetime = Field(default_factory=_now)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class EvalArtifact(BaseModel):
|
|
192
|
+
"""评估工件 — 将失败转换为可重复执行的评估"""
|
|
193
|
+
eval_id: str = Field(default_factory=lambda: f"eval_{uuid4().hex[:12]}")
|
|
194
|
+
origin_experience: str
|
|
195
|
+
runner: str = Field(default="pytest")
|
|
196
|
+
command: str
|
|
197
|
+
expected: dict[str, Any] = Field(default_factory=lambda: {"exit_code": 0})
|
|
198
|
+
generated_by: str = Field(default="agent")
|
|
199
|
+
review: Optional[dict[str, Any]] = None
|
|
200
|
+
last_result: Optional[dict[str, Any]] = None
|
|
201
|
+
created_at: datetime = Field(default_factory=_now)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CounterfactualReplay(BaseModel):
|
|
205
|
+
"""反事实回放 — 记录替代策略是否得到了验证"""
|
|
206
|
+
replay_id: str = Field(default_factory=lambda: f"replay_{uuid4().hex[:12]}")
|
|
207
|
+
experience_id: str
|
|
208
|
+
original_strategy: str
|
|
209
|
+
alternative_strategy: str
|
|
210
|
+
verification_mode: str = Field(default="sandbox_replay") # predicted | sandbox_replay | production
|
|
211
|
+
result: dict[str, Any] = Field(default_factory=lambda: {
|
|
212
|
+
"status": "unknown", "objective_delta": {}
|
|
213
|
+
})
|
|
214
|
+
created_at: datetime = Field(default_factory=_now)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class LessonDelivery(BaseModel):
|
|
218
|
+
"""Lesson 交付 — 记录 Lesson 如何进入 Agent 上下文及是否被采纳"""
|
|
219
|
+
delivery_id: str = Field(default_factory=lambda: f"delivery_{uuid4().hex[:12]}")
|
|
220
|
+
lesson_id: str
|
|
221
|
+
experience_id: str
|
|
222
|
+
strategy: DeliveryStrategy
|
|
223
|
+
delivered_at: datetime = Field(default_factory=_now)
|
|
224
|
+
delivery_context: Optional[str] = None
|
|
225
|
+
acknowledged: bool = False
|
|
226
|
+
applied: bool = False
|
|
227
|
+
application_evidence_refs: list[str] = Field(default_factory=list)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class LessonEvaluation(BaseModel):
|
|
231
|
+
"""Lesson 效果评估 — 记录效果、实验设计、负面证据与处置"""
|
|
232
|
+
evaluation_id: str = Field(default_factory=lambda: f"leval_{uuid4().hex[:12]}")
|
|
233
|
+
lesson_id: str
|
|
234
|
+
method: EvaluationMethod
|
|
235
|
+
population: dict[str, Any] = Field(default_factory=dict)
|
|
236
|
+
results: dict[str, Any] = Field(default_factory=lambda: {
|
|
237
|
+
"with_lesson": {"tasks": 0, "successes": 0},
|
|
238
|
+
"baseline": {"tasks": 0, "successes": 0},
|
|
239
|
+
"estimated_effect": None,
|
|
240
|
+
"uncertainty_interval": None,
|
|
241
|
+
})
|
|
242
|
+
decision: str = Field(default="keep_active") # keep_active | restrict_scope | review | deprecate | reject
|
|
243
|
+
evidence_refs: list[str] = Field(default_factory=list)
|
|
244
|
+
created_at: datetime = Field(default_factory=_now)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class LessonRollback(BaseModel):
|
|
248
|
+
"""Lesson 回滚 — 坏 Lesson 撤回的审计记录"""
|
|
249
|
+
rollback_id: str = Field(default_factory=lambda: f"rollback_{uuid4().hex[:12]}")
|
|
250
|
+
lesson_id: str
|
|
251
|
+
reason: str
|
|
252
|
+
previous_status: LessonStatus
|
|
253
|
+
new_status: LessonStatus
|
|
254
|
+
affected_deliveries: list[str] = Field(default_factory=list)
|
|
255
|
+
replacement_lesson_id: Optional[str] = None
|
|
256
|
+
evidence_refs: list[str] = Field(default_factory=list)
|
|
257
|
+
created_at: datetime = Field(default_factory=_now)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TrainingCandidate(BaseModel):
|
|
261
|
+
"""训练候选 — 将经验转化为训练资产的审批通道"""
|
|
262
|
+
candidate_id: str = Field(default_factory=lambda: f"train_{uuid4().hex[:12]}")
|
|
263
|
+
source_experience_ids: list[str] = Field(default_factory=list)
|
|
264
|
+
format: TrainingFormat
|
|
265
|
+
validation: dict[str, bool] = Field(default_factory=lambda: {
|
|
266
|
+
"outcome_verified": False,
|
|
267
|
+
"human_reviewed": False,
|
|
268
|
+
"privacy_reviewed": False,
|
|
269
|
+
"license_reviewed": False,
|
|
270
|
+
})
|
|
271
|
+
status: TrainingStatus = TrainingStatus.CANDIDATE
|
|
272
|
+
artifact_ref: Optional[str] = None
|
|
273
|
+
created_at: datetime = Field(default_factory=_now)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ─── High-level Schema Validation ────────────────────────────
|
|
277
|
+
|
|
278
|
+
def validate_lesson_scope(lesson: Lesson) -> list[str]:
|
|
279
|
+
"""检查 Lesson 的作用域定义是否完整"""
|
|
280
|
+
issues = []
|
|
281
|
+
if not lesson.scope.get("task_domains"):
|
|
282
|
+
issues.append("Lesson missing task_domains in scope")
|
|
283
|
+
if not lesson.trigger.get("conditions"):
|
|
284
|
+
issues.append("Lesson missing trigger conditions")
|
|
285
|
+
return issues
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def check_lesson_conflict(a: Lesson, b: Lesson) -> bool:
|
|
289
|
+
"""检查两条 Lesson 是否有冲突
|
|
290
|
+
|
|
291
|
+
先比较 scope,再比较建议内容。
|
|
292
|
+
不同 scope 的两条建议即使语义相反也不应判定为冲突。
|
|
293
|
+
"""
|
|
294
|
+
# 如果 scope 完全不重叠,不算冲突
|
|
295
|
+
a_domains = set(a.scope.get("task_domains", []))
|
|
296
|
+
b_domains = set(b.scope.get("task_domains", []))
|
|
297
|
+
if a_domains and b_domains and not a_domains & b_domains:
|
|
298
|
+
return False
|
|
299
|
+
a_versions = set(a.scope.get("agent_versions", []))
|
|
300
|
+
b_versions = set(b.scope.get("agent_versions", []))
|
|
301
|
+
if a_versions and b_versions and not a_versions & b_versions:
|
|
302
|
+
return False
|
|
303
|
+
return True
|