|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
from copy import deepcopy
|
|
from copy import deepcopy
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
|
|
+import json
|
|
|
from typing import Any, Literal
|
|
from typing import Any, Literal
|
|
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
@@ -34,6 +35,35 @@ class StrictProtocolModel(BaseModel):
|
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class RootTaskAnchor(StrictProtocolModel):
|
|
|
|
|
+ """Recursive 根任务不可变的目标锚点。
|
|
|
|
|
+
|
|
|
|
|
+ 调用方创建根 Trace 时提供;Runner 固化后由每层孩子和 Validator读取,
|
|
|
|
|
+ 中间 Agent不能重新概括或覆盖它。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ objective: str = Field(min_length=1)
|
|
|
|
|
+ completion_criteria: list[str] = Field(min_length=1)
|
|
|
|
|
+ constraints: list[str] = Field(default_factory=list)
|
|
|
|
|
+
|
|
|
|
|
+ @field_validator("completion_criteria", "constraints")
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def validate_non_empty_items(cls, items: list[str]) -> list[str]:
|
|
|
|
|
+ if any(not item.strip() for item in items):
|
|
|
|
|
+ raise ValueError("items must be non-empty strings")
|
|
|
|
|
+ return items
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class ContextRef(StrictProtocolModel):
|
|
|
|
|
+ """模型可在 TaskBrief 中转交的最小上下文引用句柄。
|
|
|
|
|
+
|
|
|
|
|
+ 来源、类型、摘要和内容由框架保管;模型只能选择已获授权的 ID与版本。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ ref_id: str = Field(min_length=1)
|
|
|
|
|
+ version: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class TaskBrief(StrictProtocolModel):
|
|
class TaskBrief(StrictProtocolModel):
|
|
|
"""父 Agent 下发给直接孩子的结构化任务说明。
|
|
"""父 Agent 下发给直接孩子的结构化任务说明。
|
|
|
|
|
|
|
@@ -47,6 +77,7 @@ class TaskBrief(StrictProtocolModel):
|
|
|
parent_findings: list[str] = Field(default_factory=list)
|
|
parent_findings: list[str] = Field(default_factory=list)
|
|
|
context: dict[str, Any] = Field(default_factory=dict)
|
|
context: dict[str, Any] = Field(default_factory=dict)
|
|
|
constraints: list[str] = Field(default_factory=list)
|
|
constraints: list[str] = Field(default_factory=list)
|
|
|
|
|
+ context_refs: list[ContextRef] = Field(default_factory=list)
|
|
|
|
|
|
|
|
@field_validator(
|
|
@field_validator(
|
|
|
"completion_criteria",
|
|
"completion_criteria",
|
|
@@ -180,6 +211,9 @@ 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
|
|
brief = TaskBrief.model_validate(task_brief).model_dump() if task_brief else None
|
|
|
return {
|
|
return {
|
|
|
"task_brief": brief,
|
|
"task_brief": brief,
|
|
|
|
|
+ "task_brief_version": 1 if brief else 0,
|
|
|
|
|
+ "task_brief_effective_at_sequence": 0,
|
|
|
|
|
+ "task_brief_history": [],
|
|
|
"task_report": None,
|
|
"task_report": None,
|
|
|
"task_report_submitted_at_sequence": None,
|
|
"task_report_submitted_at_sequence": None,
|
|
|
"task_report_validation": None,
|
|
"task_report_validation": None,
|
|
@@ -195,6 +229,44 @@ def new_task_protocol(task_brief: TaskBrief | dict | None = None) -> dict[str, A
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def replace_task_brief(
|
|
|
|
|
+ state: dict[str, Any],
|
|
|
|
|
+ task_brief: TaskBrief | dict[str, Any],
|
|
|
|
|
+ *,
|
|
|
|
|
+ effective_at_sequence: int,
|
|
|
|
|
+) -> int:
|
|
|
|
|
+ """以唯一入口替换当前 TaskBrief并保留旧版本。
|
|
|
|
|
+
|
|
|
|
|
+ `review_task_result`批准修订后,`agent(continue_from=...)`真正恢复孩子时调用,
|
|
|
|
|
+ 避免审核阶段提前改写孩子,也避免覆盖后丢失任务合同的审计版本。
|
|
|
|
|
+ """
|
|
|
|
|
+ brief = TaskBrief.model_validate(task_brief).model_dump(mode="json")
|
|
|
|
|
+ current = state.get("task_brief")
|
|
|
|
|
+ current_version = state.get("task_brief_version", 0)
|
|
|
|
|
+ if not isinstance(current_version, int) or current_version < 0:
|
|
|
|
|
+ current_version = 0
|
|
|
|
|
+ if current is not None:
|
|
|
|
|
+ history_entry = {
|
|
|
|
|
+ "version": current_version or 1,
|
|
|
|
|
+ "task_brief": deepcopy(current),
|
|
|
|
|
+ "effective_at_sequence": state.get(
|
|
|
|
|
+ "task_brief_effective_at_sequence",
|
|
|
|
|
+ 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+ history = state.setdefault("task_brief_history", [])
|
|
|
|
|
+ if history_entry not in history:
|
|
|
|
|
+ history.append(history_entry)
|
|
|
|
|
+ next_version = max(current_version, 0) + 1 if current is not None else 1
|
|
|
|
|
+ state["task_brief"] = brief
|
|
|
|
|
+ state["task_brief_version"] = next_version
|
|
|
|
|
+ state["task_brief_effective_at_sequence"] = max(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ int(effective_at_sequence),
|
|
|
|
|
+ )
|
|
|
|
|
+ return next_version
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def ensure_task_protocol(context: dict[str, Any]) -> dict[str, Any]:
|
|
def ensure_task_protocol(context: dict[str, Any]) -> dict[str, Any]:
|
|
|
"""获取 Trace context 中的协议状态,并幂等补齐缺失字段。
|
|
"""获取 Trace context 中的协议状态,并幂等补齐缺失字段。
|
|
|
|
|
|
|
@@ -297,7 +369,11 @@ def pending_review_entry(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def format_task_brief(task_brief: TaskBrief) -> str:
|
|
|
|
|
|
|
+def format_task_brief(
|
|
|
|
|
+ task_brief: TaskBrief,
|
|
|
|
|
+ *,
|
|
|
|
|
+ available_context_refs: list[dict[str, Any]] | None = None,
|
|
|
|
|
+) -> str:
|
|
|
"""将结构化 TaskBrief 格式化为子 Agent 可执行的任务文本。
|
|
"""将结构化 TaskBrief 格式化为子 Agent 可执行的任务文本。
|
|
|
|
|
|
|
|
``agent`` 完成规范化后调用,生成 Runner 作为 user message 传给子 Agent 的任务说明。
|
|
``agent`` 完成规范化后调用,生成 Runner 作为 user message 传给子 Agent 的任务说明。
|
|
@@ -323,9 +399,27 @@ def format_task_brief(task_brief: TaskBrief) -> str:
|
|
|
if parent_findings:
|
|
if parent_findings:
|
|
|
parts.append(f"\n## Parent Findings\n{parent_findings}")
|
|
parts.append(f"\n## Parent Findings\n{parent_findings}")
|
|
|
if task_brief.context:
|
|
if task_brief.context:
|
|
|
- parts.append(f"\n## Context\n{task_brief.context}")
|
|
|
|
|
|
|
+ parts.append(
|
|
|
|
|
+ "\n## Context\n"
|
|
|
|
|
+ + json.dumps(
|
|
|
|
|
+ task_brief.context,
|
|
|
|
|
+ ensure_ascii=False,
|
|
|
|
|
+ sort_keys=True,
|
|
|
|
|
+ separators=(",", ":"),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
if constraints:
|
|
if constraints:
|
|
|
parts.append(f"\n## Constraints\n{constraints}")
|
|
parts.append(f"\n## Constraints\n{constraints}")
|
|
|
|
|
+ if available_context_refs:
|
|
|
|
|
+ parts.append(
|
|
|
|
|
+ "\n## Available Context References\n"
|
|
|
|
|
+ + json.dumps(
|
|
|
|
|
+ available_context_refs,
|
|
|
|
|
+ ensure_ascii=False,
|
|
|
|
|
+ sort_keys=True,
|
|
|
|
|
+ separators=(",", ":"),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
parts.append(
|
|
parts.append(
|
|
|
"\nSubmit a validated TaskReport with submit_task_report before finishing."
|
|
"\nSubmit a validated TaskReport with submit_task_report before finishing."
|
|
|
)
|
|
)
|