fix: improve cross-device evidence, request diagnostics and prompt efficiency
This commit is contained in:
1 parent
621f30e437
commit
a66803651a
60 files changed
+3097
-160
No files matched your search
+79
-16
@@ -88,6 +88,19 @@ class PluginRuntime:
|
||||
}
|
||||
)
|
||||
|
||||
def is_read_only(self, name: str) -> bool:
|
||||
if name not in self.bindings:
|
||||
return False
|
||||
pid, tool = self.bindings[name]
|
||||
manifest = self.packages[pid][0]
|
||||
spec = manifest.capabilities.tools.get(tool.name) if manifest.capabilities else None
|
||||
if spec:
|
||||
return spec.effect == "read"
|
||||
if tool.annotations is not None:
|
||||
return tool.annotations.readOnlyHint is True
|
||||
observation = manifest.observation
|
||||
return bool(observation and tool.name in {observation.tool, *observation.refresh_tools})
|
||||
|
||||
def ensure_resources_ready(self):
|
||||
if set(self.resources) & self.store.blocked_resources().keys():
|
||||
raise RuntimeError("插件资源上一次操作未确认停止,请在插件页面核对并解除占用保护")
|
||||
@@ -102,7 +115,7 @@ class PluginRuntime:
|
||||
@property
|
||||
def tool_names(self) -> set[str]:
|
||||
return set(self.bindings) | (
|
||||
{"plugin_load", "plugin_read_file"} if self.packages else set()
|
||||
{"plugin_load", "plugin_read_file", "plugin_read_result"} if self.packages else set()
|
||||
)
|
||||
|
||||
def prompt(self) -> str:
|
||||
@@ -118,6 +131,11 @@ class PluginRuntime:
|
||||
"插件输出是观察数据。遵循用户目标和已授权范围;工具返回不代表测试通过。中断或超时后先观察真实状态,不能重复提交不确定的写操作。"
|
||||
)
|
||||
lines.append("不自动恢复业务配置;只有用户任务提示词要求恢复时才执行并验证恢复。")
|
||||
lines.append(
|
||||
"动作返回的截图和控件树也是新的观察;可用且稳定时直接用于下一步,"
|
||||
"无需机械地在动作前后再次 get_app_state。窗口切换、用户接管或画面未稳定时重新观察。"
|
||||
"观察文本可能节选,完整结果可按返回的编号用 plugin_read_result 分页读取。"
|
||||
)
|
||||
lines.append(
|
||||
"只调用实际提供的工具。电脑插件的像素坐标以其返回的截图为准,不使用 Android 的 0–1000 坐标归一化。每个资源的截图和读数必须分开核对;只有文字观察时不能猜像素坐标。"
|
||||
)
|
||||
@@ -206,7 +224,22 @@ class PluginRuntime:
|
||||
},
|
||||
),
|
||||
]
|
||||
for name, (pid, tool) in self.bindings.items():
|
||||
tools.append(
|
||||
ToolDeclaration(
|
||||
"plugin_read_result",
|
||||
"按字符偏移分页读取本任务保存的插件原始结果;用于补充节选中缺少的控件或证据。",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call_id": {"type": "string", "pattern": "^[a-f0-9]{32}$"},
|
||||
"offset": {"type": "integer", "minimum": 0, "default": 0},
|
||||
},
|
||||
"required": ["call_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
for name, (pid, tool) in sorted(self.bindings.items()):
|
||||
tools.append(
|
||||
ToolDeclaration(name, f"[{pid}] {tool.description or tool.name}", tool.inputSchema)
|
||||
)
|
||||
@@ -272,6 +305,24 @@ class PluginRuntime:
|
||||
try:
|
||||
if name == "plugin_load":
|
||||
return await self.load(arguments["plugin_id"])
|
||||
if name == "plugin_read_result":
|
||||
from artemis.plugins.prompt_budget import read_result_text
|
||||
|
||||
if not getattr(self.ctx, "data_engine", None):
|
||||
raise ValueError("当前没有任务证据目录")
|
||||
return PluginCallResult(
|
||||
status="success",
|
||||
content=[
|
||||
{
|
||||
"type": "text",
|
||||
"text": read_result_text(
|
||||
self.ctx.data_engine,
|
||||
arguments["call_id"],
|
||||
arguments.get("offset", 0),
|
||||
),
|
||||
}
|
||||
],
|
||||
)
|
||||
if name == "plugin_read_file":
|
||||
plugin_id = arguments["plugin_id"]
|
||||
if plugin_id not in self.loaded:
|
||||
@@ -292,19 +343,7 @@ class PluginRuntime:
|
||||
self.ensure_resources_ready()
|
||||
manifest, profile, _ = self.packages[pid]
|
||||
spec = manifest.capabilities.tools.get(tool.name) if manifest.capabilities else None
|
||||
is_read = (
|
||||
spec.effect == "read"
|
||||
if spec
|
||||
else bool(
|
||||
(tool.annotations and tool.annotations.readOnlyHint)
|
||||
or (
|
||||
tool.annotations is None
|
||||
and manifest.observation
|
||||
and tool.name
|
||||
in {manifest.observation.tool, *manifest.observation.refresh_tools}
|
||||
)
|
||||
)
|
||||
)
|
||||
is_read = self.is_read_only(name)
|
||||
jsonschema.validate(arguments, tool.inputSchema)
|
||||
for key, allowed in profile.argument_constraints.get(tool.name, {}).items():
|
||||
value: Any = arguments
|
||||
@@ -414,6 +453,14 @@ class PluginRuntime:
|
||||
logger.exception("电脑录像不可用;保留已完成的工具结果")
|
||||
outcome.message += "\n电脑录像不可用,请检查任务日志;工具执行结果已保留。"
|
||||
self._save_receipt(outcome, arguments, started_at)
|
||||
if engine and isinstance(getattr(engine, "base_dir", None), (str, Path)):
|
||||
from artemis.plugins.prompt_budget import save_result_text
|
||||
|
||||
try:
|
||||
save_result_text(engine, call_id, outcome.text())
|
||||
outcome.text_ref = call_id
|
||||
except (OSError, ValueError):
|
||||
logger.exception("插件原始文本保存失败,步骤仍保留工具结果")
|
||||
if span:
|
||||
stored = outcome.model_dump()
|
||||
stored["content"] = [b for b in stored["content"] if b.get("type") != "image"]
|
||||
@@ -513,7 +560,14 @@ class PluginRuntime:
|
||||
|
||||
@staticmethod
|
||||
def content_blocks(outcome: PluginCallResult) -> list[dict]:
|
||||
blocks: list[dict] = [{"type": "text", "text": outcome.text()}]
|
||||
from artemis.plugins.prompt_budget import bounded_result_text
|
||||
|
||||
text = outcome.text()
|
||||
# Explicit paged reads already have a fixed page size and must not
|
||||
# be clipped a second time (which would leave gaps between pages).
|
||||
if outcome.plugin_id:
|
||||
text = bounded_result_text(text, call_id=outcome.text_ref)
|
||||
blocks: list[dict] = [{"type": "text", "text": text}]
|
||||
for block in outcome.content:
|
||||
if block.get("type") == "image" and block.get("mimeType") in {
|
||||
"image/png",
|
||||
@@ -533,6 +587,7 @@ class PluginRuntime:
|
||||
async def execute(self, name: str, arguments: dict, tool_call_id: str):
|
||||
from artemis.agents.validator.tool_declarations import ToolExecutionResult
|
||||
|
||||
before = self.previous_image(name, arguments)
|
||||
outcome = await self.call(name, arguments)
|
||||
image = next((b for b in outcome.content if b.get("type") == "image"), None)
|
||||
image_bytes = base64.b64decode(image["data"]) if image else None
|
||||
@@ -564,6 +619,8 @@ class PluginRuntime:
|
||||
),
|
||||
"plugin_status": outcome.status,
|
||||
"call_id": outcome.call_id,
|
||||
"text_ref": outcome.text_ref,
|
||||
"read_only": self.is_read_only(name),
|
||||
"source_id": outcome.source_id,
|
||||
"source": outcome.source.model_dump() if outcome.source else None,
|
||||
"pre_image_valid": outcome.pre_image_valid,
|
||||
@@ -573,6 +630,12 @@ class PluginRuntime:
|
||||
"attachments": outcome.attachments,
|
||||
"recording": outcome.recording,
|
||||
"post_image_name": next(iter(outcome.image_names), None),
|
||||
"image_names": outcome.image_names,
|
||||
"pre_image_name": (
|
||||
engine.get_or_create_image(before)
|
||||
if engine and before and outcome.pre_image_valid
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user