|
|
@@ -1,4 +1,4 @@
|
|
|
-"""Recursive revision 2 的父子任务协议与持久化状态。
|
|
|
+"""Recursive revision 3 的父子任务协议与持久化状态。
|
|
|
|
|
|
Sub-Agent 使用 TaskBrief 下发任务,子 Agent 通过 TaskReport 上报,父 Agent 使用
|
|
|
TaskReview 审核。Runner 和两个协议工具共同把待审、后续动作和回退状态保存在 Trace context。
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
|
|
|
from copy import deepcopy
|
|
|
from datetime import datetime
|
|
|
+from hashlib import sha256
|
|
|
import json
|
|
|
from typing import Any, Literal
|
|
|
|
|
|
@@ -29,8 +30,26 @@ ReviewDecision = Literal[
|
|
|
"FAIL",
|
|
|
]
|
|
|
TaskValidationScope = Literal["evidence", "hypothesis", "output", "task"]
|
|
|
+TaskProgressPhase = Literal[
|
|
|
+ "planning",
|
|
|
+ "investigating",
|
|
|
+ "forming_hypothesis",
|
|
|
+ "executing",
|
|
|
+ "producing_output",
|
|
|
+ "ready_to_submit",
|
|
|
+ "blocked",
|
|
|
+]
|
|
|
_VALIDATION_SCOPE_ORDER = ("evidence", "hypothesis", "output", "task")
|
|
|
|
|
|
+TASK_PROTOCOL_SCHEMA_VERSION = 1
|
|
|
+MAX_TASK_PROGRESS_ITEMS_PER_KIND = 20
|
|
|
+MAX_TASK_PROGRESS_ITEMS = 60
|
|
|
+MAX_TASK_PROGRESS_CHARS = 24_000
|
|
|
+MAX_TASK_PROGRESS_REVISIONS = 64
|
|
|
+MAX_TASK_PROGRESS_HISTORY_CHARS = 512_000
|
|
|
+MAX_TASK_PROGRESS_CONTEXT_REFS = 8
|
|
|
+MAX_TASK_PROGRESS_ARTIFACT_REFS = 20
|
|
|
+
|
|
|
|
|
|
class StrictProtocolModel(BaseModel):
|
|
|
"""协议模型的统一严格基类,拒绝未定义字段并清理字符串空白。"""
|
|
|
@@ -232,6 +251,386 @@ class TaskReview(StrictProtocolModel):
|
|
|
return self
|
|
|
|
|
|
|
|
|
+class TaskContractRef(StrictProtocolModel):
|
|
|
+ """TaskProgress 绑定的不可变根目标或 TaskBrief 合同。"""
|
|
|
+
|
|
|
+ kind: Literal["root_task_anchor", "task_brief"]
|
|
|
+ root_task_anchor_hash: str | None = Field(
|
|
|
+ default=None,
|
|
|
+ pattern=r"^[0-9a-f]{64}$",
|
|
|
+ )
|
|
|
+ task_brief_version: int | None = Field(default=None, ge=1)
|
|
|
+ task_brief_hash: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_kind_fields(self) -> "TaskContractRef":
|
|
|
+ if self.kind == "root_task_anchor":
|
|
|
+ if not self.root_task_anchor_hash:
|
|
|
+ raise ValueError("root_task_anchor requires root_task_anchor_hash")
|
|
|
+ if self.task_brief_version is not None or self.task_brief_hash is not None:
|
|
|
+ raise ValueError("root_task_anchor does not accept TaskBrief fields")
|
|
|
+ else:
|
|
|
+ if self.task_brief_version is None or not self.task_brief_hash:
|
|
|
+ raise ValueError("task_brief requires version and hash")
|
|
|
+ if self.root_task_anchor_hash is not None:
|
|
|
+ raise ValueError("task_brief does not accept root_task_anchor_hash")
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class ProgressRefs(StrictProtocolModel):
|
|
|
+ """任务推进条目可引用的现有上下文与产物句柄。"""
|
|
|
+
|
|
|
+ context_refs: list[ContextRef] = Field(default_factory=list, max_length=8)
|
|
|
+ artifact_refs: list[ArtifactRef] = Field(default_factory=list, max_length=20)
|
|
|
+
|
|
|
+
|
|
|
+class Question(ProgressRefs):
|
|
|
+ item_id: str = Field(min_length=1, max_length=100)
|
|
|
+ text: str = Field(min_length=1, max_length=2_000)
|
|
|
+ state: Literal["open", "answered", "deferred"] = "open"
|
|
|
+ answer: str | None = Field(default=None, min_length=1, max_length=2_000)
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_answer(self) -> "Question":
|
|
|
+ if self.state == "answered" and not self.answer:
|
|
|
+ raise ValueError("answered question requires answer")
|
|
|
+ if self.state != "answered" and self.answer is not None:
|
|
|
+ raise ValueError("only answered question accepts answer")
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class Blocker(ProgressRefs):
|
|
|
+ item_id: str = Field(min_length=1, max_length=100)
|
|
|
+ text: str = Field(min_length=1, max_length=2_000)
|
|
|
+ state: Literal["active", "resolved"] = "active"
|
|
|
+ resolution: str | None = Field(default=None, min_length=1, max_length=2_000)
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_resolution(self) -> "Blocker":
|
|
|
+ if self.state == "resolved" and not self.resolution:
|
|
|
+ raise ValueError("resolved blocker requires resolution")
|
|
|
+ if self.state != "resolved" and self.resolution is not None:
|
|
|
+ raise ValueError("only resolved blocker accepts resolution")
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class Finding(ProgressRefs):
|
|
|
+ item_id: str = Field(min_length=1, max_length=100)
|
|
|
+ statement: str = Field(min_length=1, max_length=2_000)
|
|
|
+ basis: str = Field(min_length=1, max_length=2_000)
|
|
|
+
|
|
|
+
|
|
|
+class Hypothesis(ProgressRefs):
|
|
|
+ item_id: str = Field(min_length=1, max_length=100)
|
|
|
+ statement: str = Field(min_length=1, max_length=2_000)
|
|
|
+ state: Literal["open", "supported", "rejected"] = "open"
|
|
|
+ rationale: str | None = Field(default=None, min_length=1, max_length=2_000)
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_rationale(self) -> "Hypothesis":
|
|
|
+ if self.state in {"supported", "rejected"} and not self.rationale:
|
|
|
+ raise ValueError(f"{self.state} hypothesis requires rationale")
|
|
|
+ if self.state == "open" and self.rationale is not None:
|
|
|
+ raise ValueError("open hypothesis does not accept rationale")
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class WorkItem(ProgressRefs):
|
|
|
+ item_id: str = Field(min_length=1, max_length=100)
|
|
|
+ description: str = Field(min_length=1, max_length=2_000)
|
|
|
+ state: Literal["pending", "in_progress", "done", "abandoned"] = "pending"
|
|
|
+ result_summary: str | None = Field(default=None, min_length=1, max_length=2_000)
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_result(self) -> "WorkItem":
|
|
|
+ if self.state == "done" and not self.result_summary:
|
|
|
+ raise ValueError("done work item requires result_summary")
|
|
|
+ if self.state in {"pending", "in_progress"} and self.result_summary is not None:
|
|
|
+ raise ValueError(f"{self.state} work item does not accept result_summary")
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class TaskProgress(StrictProtocolModel):
|
|
|
+ """Agent 提交的当前 Task 完整推进快照。"""
|
|
|
+
|
|
|
+ phase: TaskProgressPhase
|
|
|
+ questions: list[Question] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ max_length=MAX_TASK_PROGRESS_ITEMS_PER_KIND,
|
|
|
+ )
|
|
|
+ blockers: list[Blocker] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ max_length=MAX_TASK_PROGRESS_ITEMS_PER_KIND,
|
|
|
+ )
|
|
|
+ findings: list[Finding] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ max_length=MAX_TASK_PROGRESS_ITEMS_PER_KIND,
|
|
|
+ )
|
|
|
+ hypotheses: list[Hypothesis] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ max_length=MAX_TASK_PROGRESS_ITEMS_PER_KIND,
|
|
|
+ )
|
|
|
+ work_items: list[WorkItem] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ max_length=MAX_TASK_PROGRESS_ITEMS_PER_KIND,
|
|
|
+ )
|
|
|
+ decision_rationale: str = Field(default="", max_length=2_000)
|
|
|
+
|
|
|
+ @model_validator(mode="after")
|
|
|
+ def validate_progress_limits(self) -> "TaskProgress":
|
|
|
+ groups = (
|
|
|
+ self.questions,
|
|
|
+ self.blockers,
|
|
|
+ self.findings,
|
|
|
+ self.hypotheses,
|
|
|
+ self.work_items,
|
|
|
+ )
|
|
|
+ items = [item for group in groups for item in group]
|
|
|
+ if len(items) > MAX_TASK_PROGRESS_ITEMS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress accepts at most {MAX_TASK_PROGRESS_ITEMS} total items"
|
|
|
+ )
|
|
|
+ item_ids = [item.item_id for item in items]
|
|
|
+ if len(set(item_ids)) != len(item_ids):
|
|
|
+ raise ValueError("TaskProgress item_id values must be globally unique")
|
|
|
+ context_refs = {
|
|
|
+ (ref.ref_id, ref.version)
|
|
|
+ for item in items
|
|
|
+ for ref in item.context_refs
|
|
|
+ }
|
|
|
+ artifact_refs = {
|
|
|
+ (ref.artifact_id, ref.version, ref.content_hash)
|
|
|
+ for item in items
|
|
|
+ for ref in item.artifact_refs
|
|
|
+ }
|
|
|
+ if len(context_refs) > MAX_TASK_PROGRESS_CONTEXT_REFS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress accepts at most {MAX_TASK_PROGRESS_CONTEXT_REFS} ContextRefs"
|
|
|
+ )
|
|
|
+ if len(artifact_refs) > MAX_TASK_PROGRESS_ARTIFACT_REFS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress accepts at most {MAX_TASK_PROGRESS_ARTIFACT_REFS} ArtifactRefs"
|
|
|
+ )
|
|
|
+ encoded = _canonical_json(self.model_dump(mode="json"))
|
|
|
+ if len(encoded) > MAX_TASK_PROGRESS_CHARS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress exceeds {MAX_TASK_PROGRESS_CHARS} characters"
|
|
|
+ )
|
|
|
+ return self
|
|
|
+
|
|
|
+
|
|
|
+class TaskProgressRevision(TaskProgress):
|
|
|
+ """框架注入版本、时间线与合同引用后的持久化记录。"""
|
|
|
+
|
|
|
+ schema_version: Literal[1] = 1
|
|
|
+ revision: int = Field(ge=1)
|
|
|
+ parent_revision: int | None = Field(default=None, ge=1)
|
|
|
+ effective_at_sequence: int = Field(ge=0)
|
|
|
+ task_contract_ref: TaskContractRef
|
|
|
+
|
|
|
+
|
|
|
+def _canonical_json(value: Any) -> str:
|
|
|
+ try:
|
|
|
+ return json.dumps(
|
|
|
+ value,
|
|
|
+ ensure_ascii=False,
|
|
|
+ sort_keys=True,
|
|
|
+ separators=(",", ":"),
|
|
|
+ allow_nan=False,
|
|
|
+ )
|
|
|
+ except (TypeError, ValueError) as exc:
|
|
|
+ raise ValueError(f"Task protocol data must be JSON serializable: {exc}") from exc
|
|
|
+
|
|
|
+
|
|
|
+def task_brief_hash(task_brief: TaskBrief | dict[str, Any]) -> str:
|
|
|
+ brief = TaskBrief.model_validate(task_brief)
|
|
|
+ return sha256(
|
|
|
+ _canonical_json(brief.model_dump(mode="json")).encode("utf-8")
|
|
|
+ ).hexdigest()
|
|
|
+
|
|
|
+
|
|
|
+def task_contract_ref(
|
|
|
+ state: dict[str, Any],
|
|
|
+ *,
|
|
|
+ root_task_anchor_hash: str | None = None,
|
|
|
+) -> TaskContractRef:
|
|
|
+ brief = state.get("task_brief")
|
|
|
+ if brief is None:
|
|
|
+ if not root_task_anchor_hash:
|
|
|
+ raise ValueError("root TaskProgress requires root_task_anchor_hash")
|
|
|
+ return TaskContractRef(
|
|
|
+ kind="root_task_anchor",
|
|
|
+ root_task_anchor_hash=root_task_anchor_hash,
|
|
|
+ )
|
|
|
+ version = state.get("task_brief_version")
|
|
|
+ return TaskContractRef(
|
|
|
+ kind="task_brief",
|
|
|
+ task_brief_version=version,
|
|
|
+ task_brief_hash=task_brief_hash(brief),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def current_task_progress(state: dict[str, Any]) -> TaskProgressRevision | None:
|
|
|
+ head = state.get("task_progress_head_revision")
|
|
|
+ if head is None:
|
|
|
+ return None
|
|
|
+ matches = [
|
|
|
+ item
|
|
|
+ for item in state.get("task_progress_revisions", [])
|
|
|
+ if item.get("revision") == head
|
|
|
+ ]
|
|
|
+ if len(matches) != 1:
|
|
|
+ raise ValueError("TaskProgress head does not reference exactly one revision")
|
|
|
+ return TaskProgressRevision.model_validate(matches[0])
|
|
|
+
|
|
|
+
|
|
|
+def task_progress_at_revision(
|
|
|
+ state: dict[str, Any],
|
|
|
+ revision: int | None,
|
|
|
+) -> TaskProgressRevision | None:
|
|
|
+ if revision is None:
|
|
|
+ return None
|
|
|
+ matches = [
|
|
|
+ raw
|
|
|
+ for raw in state.get("task_progress_revisions", [])
|
|
|
+ if raw.get("revision") == revision
|
|
|
+ ]
|
|
|
+ if len(matches) != 1:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress revision {revision} does not reference exactly one record"
|
|
|
+ )
|
|
|
+ return TaskProgressRevision.model_validate(matches[0])
|
|
|
+
|
|
|
+
|
|
|
+def task_progress_artifact_refs(
|
|
|
+ progress: TaskProgress | TaskProgressRevision | None,
|
|
|
+) -> list[ArtifactRef]:
|
|
|
+ if progress is None:
|
|
|
+ return []
|
|
|
+ unique: dict[tuple[str, str, str], ArtifactRef] = {}
|
|
|
+ for item in (
|
|
|
+ *progress.questions,
|
|
|
+ *progress.blockers,
|
|
|
+ *progress.findings,
|
|
|
+ *progress.hypotheses,
|
|
|
+ *progress.work_items,
|
|
|
+ ):
|
|
|
+ for ref in item.artifact_refs:
|
|
|
+ unique[(ref.artifact_id, ref.version, ref.content_hash)] = ref
|
|
|
+ return list(unique.values())
|
|
|
+
|
|
|
+
|
|
|
+def append_task_progress_revision(
|
|
|
+ state: dict[str, Any],
|
|
|
+ progress: TaskProgress | dict[str, Any],
|
|
|
+ *,
|
|
|
+ expected_revision: int,
|
|
|
+ effective_at_sequence: int,
|
|
|
+ contract_ref: TaskContractRef,
|
|
|
+) -> TaskProgressRevision:
|
|
|
+ """校验并追加一个完整快照;校验失败时不修改 state。"""
|
|
|
+ revisions = list(state.get("task_progress_revisions", []))
|
|
|
+ head = state.get("task_progress_head_revision")
|
|
|
+ current_revision = int(head or 0)
|
|
|
+ if expected_revision != current_revision:
|
|
|
+ raise ValueError(
|
|
|
+ f"stale TaskProgress revision: expected={expected_revision}, current={current_revision}"
|
|
|
+ )
|
|
|
+ if len(revisions) >= MAX_TASK_PROGRESS_REVISIONS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress accepts at most {MAX_TASK_PROGRESS_REVISIONS} revisions"
|
|
|
+ )
|
|
|
+ parsed = TaskProgress.model_validate(progress)
|
|
|
+ known_revisions = [int(item.get("revision", 0)) for item in revisions]
|
|
|
+ next_revision = max(known_revisions, default=0) + 1
|
|
|
+ revision = TaskProgressRevision(
|
|
|
+ **parsed.model_dump(mode="json"),
|
|
|
+ revision=next_revision,
|
|
|
+ parent_revision=head,
|
|
|
+ effective_at_sequence=max(0, int(effective_at_sequence)),
|
|
|
+ task_contract_ref=contract_ref,
|
|
|
+ )
|
|
|
+ candidate = [*revisions, revision.model_dump(mode="json")]
|
|
|
+ if len(_canonical_json(candidate)) > MAX_TASK_PROGRESS_HISTORY_CHARS:
|
|
|
+ raise ValueError(
|
|
|
+ f"TaskProgress history exceeds {MAX_TASK_PROGRESS_HISTORY_CHARS} characters"
|
|
|
+ )
|
|
|
+ state["task_progress_revisions"] = candidate
|
|
|
+ state["task_progress_head_revision"] = revision.revision
|
|
|
+ return revision
|
|
|
+
|
|
|
+
|
|
|
+def initialize_task_progress(
|
|
|
+ state: dict[str, Any],
|
|
|
+ *,
|
|
|
+ root_task_anchor_hash: str | None = None,
|
|
|
+ effective_at_sequence: int = 0,
|
|
|
+) -> TaskProgressRevision:
|
|
|
+ existing = current_task_progress(state)
|
|
|
+ if existing is not None:
|
|
|
+ return existing
|
|
|
+ return append_task_progress_revision(
|
|
|
+ state,
|
|
|
+ TaskProgress(phase="planning"),
|
|
|
+ expected_revision=0,
|
|
|
+ effective_at_sequence=effective_at_sequence,
|
|
|
+ contract_ref=task_contract_ref(
|
|
|
+ state,
|
|
|
+ root_task_anchor_hash=root_task_anchor_hash,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def task_progress_readiness_error(state: dict[str, Any]) -> str | None:
|
|
|
+ progress = current_task_progress(state)
|
|
|
+ if progress is None:
|
|
|
+ return "TaskProgress is required"
|
|
|
+ if progress.phase != "ready_to_submit":
|
|
|
+ return "TaskProgress phase must be ready_to_submit"
|
|
|
+ if any(item.state == "open" for item in progress.questions):
|
|
|
+ return "TaskProgress has open questions"
|
|
|
+ if any(item.state == "active" for item in progress.blockers):
|
|
|
+ return "TaskProgress has active blockers"
|
|
|
+ if any(item.state == "open" for item in progress.hypotheses):
|
|
|
+ return "TaskProgress has open hypotheses"
|
|
|
+ if any(item.state in {"pending", "in_progress"} for item in progress.work_items):
|
|
|
+ return "TaskProgress has unfinished work items"
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def rewind_task_progress(state: dict[str, Any], cutoff: int) -> int | None:
|
|
|
+ """只沿当前 head 的祖先链回溯,不删除其他分支。"""
|
|
|
+ by_revision = {
|
|
|
+ item.revision: item
|
|
|
+ for item in (
|
|
|
+ TaskProgressRevision.model_validate(raw)
|
|
|
+ for raw in state.get("task_progress_revisions", [])
|
|
|
+ )
|
|
|
+ }
|
|
|
+ head = state.get("task_progress_head_revision")
|
|
|
+ visited: set[int] = set()
|
|
|
+ while head is not None:
|
|
|
+ if head in visited or head not in by_revision:
|
|
|
+ raise ValueError("TaskProgress revision ancestry is corrupt")
|
|
|
+ visited.add(head)
|
|
|
+ revision = by_revision[head]
|
|
|
+ if revision.effective_at_sequence <= cutoff:
|
|
|
+ break
|
|
|
+ head = revision.parent_revision
|
|
|
+ state["task_progress_head_revision"] = head
|
|
|
+ retained_ancestry: set[int] = set()
|
|
|
+ cursor = head
|
|
|
+ while cursor is not None:
|
|
|
+ if cursor in retained_ancestry or cursor not in by_revision:
|
|
|
+ raise ValueError("TaskProgress revision ancestry is corrupt")
|
|
|
+ retained_ancestry.add(cursor)
|
|
|
+ cursor = by_revision[cursor].parent_revision
|
|
|
+ report_revision = state.get("task_report_progress_revision")
|
|
|
+ if report_revision is not None and report_revision not in retained_ancestry:
|
|
|
+ state["task_report_progress_revision"] = None
|
|
|
+ return head
|
|
|
+
|
|
|
+
|
|
|
def new_task_protocol(task_brief: TaskBrief | dict | None = None) -> dict[str, Any]:
|
|
|
"""创建一份完整的 Recursive 任务协议初始状态。
|
|
|
|
|
|
@@ -239,6 +638,7 @@ def new_task_protocol(task_brief: TaskBrief | dict | None = None) -> dict[str, A
|
|
|
"""
|
|
|
brief = TaskBrief.model_validate(task_brief).model_dump() if task_brief else None
|
|
|
return {
|
|
|
+ "schema_version": TASK_PROTOCOL_SCHEMA_VERSION,
|
|
|
"task_brief": brief,
|
|
|
"task_brief_version": 1 if brief else 0,
|
|
|
"task_brief_effective_at_sequence": 0,
|
|
|
@@ -255,6 +655,9 @@ def new_task_protocol(task_brief: TaskBrief | dict | None = None) -> dict[str, A
|
|
|
"root_validation_history": [],
|
|
|
"root_validation_passed": False,
|
|
|
"protocol_correction_attempts": 0,
|
|
|
+ "task_progress_revisions": [],
|
|
|
+ "task_progress_head_revision": None,
|
|
|
+ "task_report_progress_revision": None,
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -293,6 +696,17 @@ def replace_task_brief(
|
|
|
0,
|
|
|
int(effective_at_sequence),
|
|
|
)
|
|
|
+ append_task_progress_revision(
|
|
|
+ state,
|
|
|
+ TaskProgress(phase="planning"),
|
|
|
+ expected_revision=int(state.get("task_progress_head_revision") or 0),
|
|
|
+ effective_at_sequence=effective_at_sequence,
|
|
|
+ contract_ref=TaskContractRef(
|
|
|
+ kind="task_brief",
|
|
|
+ task_brief_version=next_version,
|
|
|
+ task_brief_hash=task_brief_hash(brief),
|
|
|
+ ),
|
|
|
+ )
|
|
|
return next_version
|
|
|
|
|
|
|
|
|
@@ -308,6 +722,8 @@ def ensure_task_protocol(context: dict[str, Any]) -> dict[str, Any]:
|
|
|
defaults = new_task_protocol()
|
|
|
for key, value in defaults.items():
|
|
|
state.setdefault(key, deepcopy(value))
|
|
|
+ if state.get("schema_version") != TASK_PROTOCOL_SCHEMA_VERSION:
|
|
|
+ raise ValueError("Unsupported task_protocol schema_version")
|
|
|
return state
|
|
|
|
|
|
|