Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
"""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
|