feat: add plugin runtime and cross-device task orchestration
This commit is contained in:
1 parent
3ee4a5f7a8
commit
6663233ab8
82 files changed
+4147
-124
No files matched your search
+91
-31
@@ -202,7 +202,22 @@ class Agent:
|
||||
device_serial: str | None = None,
|
||||
retry_count: int = 5,
|
||||
retry_wait_seconds: int = 5,
|
||||
execution_target: str = "android",
|
||||
):
|
||||
if execution_target not in {"android", "computer", "mixed"}:
|
||||
raise ValueError("任务目标必须是 android、computer 或 mixed")
|
||||
self._execution_target = execution_target
|
||||
if execution_target == "computer":
|
||||
self._device_context = DeviceContext(
|
||||
device_id="computer:local",
|
||||
mobile_platform=DevicePlatform.COMPUTER,
|
||||
device_width=0,
|
||||
device_height=0,
|
||||
)
|
||||
self._adb_client = None
|
||||
self._ui_adb_client = None
|
||||
self._initialized = True
|
||||
return True
|
||||
target_dev = device_serial or device_id
|
||||
if target_dev:
|
||||
self._config = self._config.model_copy(
|
||||
@@ -366,9 +381,13 @@ class Agent:
|
||||
- .with_trace_recording() to enable trace recording
|
||||
- .with_name() to set a custom task name
|
||||
"""
|
||||
return TaskRequestBuilder[None].from_common(
|
||||
goal=goal,
|
||||
common=self._config.task_request_defaults,
|
||||
return (
|
||||
TaskRequestBuilder[None]
|
||||
.from_common(
|
||||
goal=goal,
|
||||
common=self._config.task_request_defaults,
|
||||
)
|
||||
.for_target(getattr(self, "_execution_target", "android"))
|
||||
)
|
||||
|
||||
@overload
|
||||
@@ -480,6 +499,13 @@ class Agent:
|
||||
if not self._initialized:
|
||||
raise AgentNotInitializedError()
|
||||
|
||||
if request.execution_target != getattr(self, "_execution_target", "android"):
|
||||
raise AgentTaskRequestError("任务目标与 Agent 初始化目标不一致")
|
||||
if request.execution_target == "computer" and (
|
||||
request.app_path or request.locked_app_package
|
||||
):
|
||||
raise AgentTaskRequestError("纯电脑任务不能使用 Android 安装包或应用锁定")
|
||||
|
||||
if request.profile:
|
||||
agent_profile = self._config.agent_profiles.get(request.profile)
|
||||
if agent_profile is None:
|
||||
@@ -522,7 +548,9 @@ class Agent:
|
||||
ui_adb_client=self._ui_adb_client,
|
||||
llm_config=agent_profile.llm_config,
|
||||
agent_config=self._config,
|
||||
execution_target=request.execution_target,
|
||||
)
|
||||
from artemis.plugins.runtime import PluginRuntime
|
||||
|
||||
output_config = None
|
||||
if request.output_description or request.output_format:
|
||||
@@ -551,28 +579,41 @@ class Agent:
|
||||
or getattr(task, "id", None)
|
||||
or getattr(getattr(task, "request", None), "task_name", None)
|
||||
)
|
||||
active_owner = DeviceExecutionLock.get_active_owner(self._device_context.device_id)
|
||||
already_held = (
|
||||
active_owner is not None
|
||||
and active_owner.pid == os.getpid()
|
||||
and (
|
||||
(sess_id and str(active_owner.session_id) == str(sess_id))
|
||||
or active_owner.token == self._device_context.device_id
|
||||
)
|
||||
)
|
||||
device_lock = (
|
||||
None
|
||||
if already_held
|
||||
else DeviceExecutionLock(
|
||||
self._device_context.device_id,
|
||||
description=f"{request.goal[:120]}",
|
||||
concurrency_mode=effective_mode,
|
||||
max_concurrency=effective_max,
|
||||
session_id=str(sess_id) if sess_id else None,
|
||||
ingress=os.getenv("ARTEMIS_TASK_INGRESS") or "agent",
|
||||
)
|
||||
)
|
||||
device_lock = None
|
||||
try:
|
||||
# Construct the runtime only inside the task's cleanup boundary. A
|
||||
# cancelled queue wait or setup failure must not leave version pins.
|
||||
context.plugins = PluginRuntime(request.plugins, ctx=context)
|
||||
active_owner = DeviceExecutionLock.get_active_owner(self._device_context.device_id)
|
||||
already_held = (
|
||||
active_owner is not None
|
||||
and active_owner.pid == os.getpid()
|
||||
and (
|
||||
(sess_id and str(active_owner.session_id) == str(sess_id))
|
||||
or active_owner.token == self._device_context.device_id
|
||||
)
|
||||
)
|
||||
if not already_held or context.plugins.resources:
|
||||
device_lock = DeviceExecutionLock(
|
||||
self._device_context.device_id,
|
||||
description=f"{request.goal[:120]}",
|
||||
concurrency_mode=effective_mode,
|
||||
max_concurrency=effective_max,
|
||||
session_id=str(sess_id) if sess_id else None,
|
||||
ingress=os.getenv("ARTEMIS_TASK_INGRESS") or "agent",
|
||||
resource_ids=context.plugins.resources,
|
||||
)
|
||||
context.plugins.ensure_resources_ready()
|
||||
if context.plugins.packages:
|
||||
from artemis.plugins.models import PluginSelection
|
||||
|
||||
context.plugins.store.hold(
|
||||
str(sess_id),
|
||||
[
|
||||
PluginSelection(id=m.id, version=m.version, profile=p.name)
|
||||
for m, p, _ in context.plugins.packages.values()
|
||||
],
|
||||
)
|
||||
if device_lock is not None and os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
||||
from artemis.runtime.task_control import control_dir, _write_atomic
|
||||
|
||||
@@ -615,16 +656,23 @@ class Agent:
|
||||
context.task_control = task_control
|
||||
task_control._state("running")
|
||||
self._prepare_output_files(task=task)
|
||||
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
|
||||
if (
|
||||
os.environ.get("ARTEMIS_CLOUD_MODE") != "1"
|
||||
and request.execution_target != "computer"
|
||||
):
|
||||
if self._ui_adb_client is not None:
|
||||
await self._connect_screen_client(context, str(sess_id))
|
||||
await self._ensure_device_unlocked()
|
||||
publish_startup_progress(
|
||||
"environment", "Preparing the device environment", session_id=str(sess_id)
|
||||
)
|
||||
await self._prepare_app_installation(task=task)
|
||||
await self._prepare_device_environment(context=context)
|
||||
await self._prepare_app_lock(task=task, context=context)
|
||||
if request.execution_target != "computer":
|
||||
await self._prepare_app_installation(task=task)
|
||||
await self._prepare_device_environment(context=context)
|
||||
await self._prepare_app_lock(task=task, context=context)
|
||||
elif context.execution_setup:
|
||||
context.execution_setup.video_recording_tools_enabled = False
|
||||
context.execution_setup.record_video_enabled = False
|
||||
async with context:
|
||||
recording_started = False
|
||||
recording_parts = 1
|
||||
@@ -637,6 +685,7 @@ class Agent:
|
||||
|
||||
async def pause_capture():
|
||||
nonlocal recording_started
|
||||
await context.plugins.aclose(release=False)
|
||||
if recording_started:
|
||||
try:
|
||||
await controller.stop_video_recording()
|
||||
@@ -648,6 +697,8 @@ class Agent:
|
||||
|
||||
async def resume_capture():
|
||||
nonlocal recording_started, recording_parts
|
||||
if request.execution_target == "computer":
|
||||
return
|
||||
if not (
|
||||
self._config.video_recording_tools_enabled
|
||||
or self._config.record_video_enabled
|
||||
@@ -662,7 +713,7 @@ class Agent:
|
||||
|
||||
context.on_task_pause = pause_capture
|
||||
context.on_task_resume = resume_capture
|
||||
if (
|
||||
if request.execution_target != "computer" and (
|
||||
self._config.video_recording_tools_enabled
|
||||
or self._config.record_video_enabled
|
||||
):
|
||||
@@ -897,6 +948,15 @@ class Agent:
|
||||
|
||||
raise
|
||||
finally:
|
||||
# Close plugins before releasing the whole task's device/resource lease.
|
||||
plugins_stopped = False
|
||||
try:
|
||||
if context.plugins:
|
||||
await context.plugins.aclose()
|
||||
context.plugins.store.release(str(sess_id))
|
||||
plugins_stopped = True
|
||||
except Exception:
|
||||
logger.error("插件未确认停止,执行资源将保留至工作进程退出。")
|
||||
try:
|
||||
# Background ADB processes (logcat, screenrecord, ...) started
|
||||
# by the Operator must not outlive the automation task.
|
||||
@@ -932,9 +992,9 @@ class Agent:
|
||||
if self._ui_adb_client is not None:
|
||||
await asyncio.to_thread(self._ui_adb_client.disconnect)
|
||||
finally:
|
||||
if task_control is not None:
|
||||
if task_control is not None and plugins_stopped:
|
||||
await asyncio.to_thread(task_control.release)
|
||||
elif device_lock is not None:
|
||||
elif device_lock is not None and plugins_stopped:
|
||||
await asyncio.to_thread(device_lock.release)
|
||||
|
||||
async with self._task_lock:
|
||||
|
||||
Reference in new issue
Block a user