"""Small execution-stage ledger; no extra planner/model or scheduler.""" import json from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator from artemis.core.tool_declaration import ToolDeclaration from artemis.utils.notes import save_verified_note class Stage(BaseModel): model_config = ConfigDict(extra="forbid") id: str = Field(min_length=1, max_length=80) scope: str = Field( min_length=1, max_length=600, description="Requirements covered by this stage, including its exit condition.", ) resources: list[str] = Field( min_length=1, max_length=8, description="android or actual plugin IDs; several only when the stage requires interleaving.", ) status: Literal["pending", "active", "done", "blocked"] evidence: str = Field( default="", max_length=1200, description="Observed facts and step/file references; required for done/blocked. done means scope checked, not test passed.", ) class ProgressUpdate(BaseModel): model_config = ConfigDict(extra="forbid") stages: list[Stage] = Field(min_length=1, max_length=16) reason: str = Field( min_length=1, max_length=600, description="Why the stage changed; when replanning, explain changed requirements or blocking evidence.", ) @model_validator(mode="after") def ordered(self): if len({s.id for s in self.stages}) != len(self.stages): raise ValueError("阶段 id 不能重复") if sum(s.status == "active" for s in self.stages) > 1: raise ValueError("只能有一个当前阶段") outstanding = False for stage in self.stages: if stage.status in {"done", "blocked"} and not stage.evidence.strip(): raise ValueError("完成或阻塞阶段必须记录事实与步骤/文件依据") if stage.status in {"active", "done"} and outstanding: raise ValueError("前面的阶段尚未检查完,不能提前推进;需先完成或注明阻塞") outstanding = outstanding or stage.status in {"active", "pending"} return self class StageUpdate(BaseModel): model_config = ConfigDict(extra="forbid") id: str = Field(min_length=1, max_length=80) status: Literal["pending", "active", "done", "blocked"] evidence: str | None = Field(default=None, max_length=1200) class ProgressInput(BaseModel): model_config = ConfigDict(extra="forbid") stages: list[Stage] | None = Field(default=None, min_length=1, max_length=16) stage_updates: list[StageUpdate] | None = Field(default=None, min_length=1, max_length=16) reason: str = Field(min_length=1, max_length=600) @model_validator(mode="after") def one_update_form(self): if (self.stages is None) == (self.stage_updates is None): raise ValueError("stages 与 stage_updates 必须且只能提供一种") if self.stage_updates and len({s.id for s in self.stage_updates}) != len( self.stage_updates ): raise ValueError("阶段补丁 id 不能重复") return self PROGRESS_TOOL = ToolDeclaration( "update_task_progress", "Record ordered stages for a cross-resource task before acting. Follow the user's sequence; " "read-only preflight is allowed on other resources. Update only at stage boundaries or genuine " "replanning, not every turn. Can precede the next action in the same tool-call list. " "Initialize/replan with all stages, or use stage_updates [{id,status,evidence?}] for existing IDs; " "patches preserve other stages and their order/scope/resources. Never provide both forms. " "Keep outstanding scope; done requires recorded evidence. This ledger does not verify business success.", ProgressInput.model_json_schema(), ) class TaskProgress: def __init__(self): self.value: ProgressUpdate | None = None self.last_resource: str | None = None def update(self, args: dict, *, resources: set[str], directory=None) -> str: incoming = ProgressInput.model_validate(args) stages = incoming.stages if incoming.stage_updates: if self.value is None: raise ValueError("首次记录必须提供完整 stages,不能仅提供阶段补丁") updates = {s.id: s for s in incoming.stage_updates} unknown = set(updates) - {s.id for s in self.value.stages} if unknown: raise ValueError( f"阶段补丁引用了未知 id:{sorted(unknown)};新增阶段须提供完整 stages" ) stages = [ { **stage.model_dump(), **updates[stage.id].model_dump(exclude_none=True), } if stage.id in updates else stage.model_dump() for stage in self.value.stages ] update = ProgressUpdate.model_validate({"stages": stages, "reason": incoming.reason}) if any(set(stage.resources) - resources for stage in update.stages): raise ValueError(f"阶段引用了不可用资源;可用:{sorted(resources)}") if self.value: missing = {s.id for s in self.value.stages} - {s.id for s in update.stages} if missing: raise ValueError( f"不能删除既有阶段 {sorted(missing)};保留范围并记录完成依据或阻塞原因" ) if directory: save_verified_note(directory, "task_progress", update.model_dump_json(indent=2)) self.value = update return json.dumps( { "ok": True, "active": next((s.id for s in update.stages if s.status == "active"), None), "saved": bool(directory), }, ensure_ascii=False, ) def guard(self, resource: str | None, *, completed: bool = False) -> str | None: if completed and self.value: missing = [s.scope for s in self.value.stages if s.status in {"active", "pending"}] if missing: return ( "仍有未检查完的阶段:" + "; ".join(missing) + "。先核对并更新进度,再结束任务。" ) if resource is None: return None if self.value: active = next((s for s in self.value.stages if s.status == "active"), None) if active is None or resource not in active.resources: scope = active.scope if active else "尚未选择" return f"当前阶段:{scope};暂未执行 {resource} 操作。先完成当前阶段或根据真实阻塞调整 update_task_progress;其他端只读核验仍可进行。" elif self.last_resource and resource != self.last_resource: return "跨端操作前需用 update_task_progress 记录已完成与待做范围,遵循用户指定顺序;只读预检不能直接扩展为另一端整段操作。" return None