Files
Artemis-Eapil/artemis/agents/flash/progress.py
T
chenmingxuan fac4ccb760 feat: prepare 2.2.0 with UI and execution fixes
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.
2026-10-26 00:44:13 +08:00

162 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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