Files
weilinliu55 cbdca1497c feat: extract in-process MCP action layer, concurrent task dispatch, and spawn/reconcile hardening
- Extract device action execution into artemis/mcp action layer:
  typed ActionResult, capability manifest with startup validation,
  AdbActuator as the single physical implementation, in-memory
  ActionSession (replaces stdio subprocess MCP in validator), and
  McpActionExecutor preserving the legacy executor contract.
  adb_server keeps its external stdio/SSE surface unchanged and now
  reuses the actuator implementation; import-time side effects moved
  into configure_stdio_mode().
- Capability-gated prompt assembly (prompt_assembly.py): tool enums and
  teaching segments render per available backend tools; full-set output
  is byte-identical to the previous static text (locked by tests).
- Admin console task queue: serial worker replaced by a per-device
  concurrent dispatcher (ARTEMIS_CONCURRENCY_MODE=global restores
  serial semantics), per-session cancellation, strict device binding on
  enqueue, unified stdio piping with tee to trace stdout.log, and
  shutdown cleanup of all active runs. Fix _pid_exists exception order
  so PermissionError is treated as alive.
- mcp_server reliability: spawn watchdog kills runners stuck before
  interpreter startup and releases device reservations; task state
  reconcile uses psutil liveness, prefers runner-reported pids, and
  adds a 45s startup grace to stop false failure reports on Windows;
  conversation_id is now optional end-to-end; explicit device_serial is
  validated at submission.
- CLI install now writes ~/.claude/rules/artemis.md for Claude Code and
  migrates/removes the legacy CLAUDE.md injection block; ~/.claude.json
  is never rewritten when it fails to parse.
- Tests: Windows-portable stdio lifecycle and path assertions; new
  suites for action layer, prompt assembly, spawn watchdog, and task
  reconcile (895 passed, 3 skipped).
2026-08-27 18:25:27 -07:00

80 lines
3.0 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helpers for assembling tool references into otherwise-static prompts.
Prompts stay hand-written text; only the spots that mention tools are assembled:
* **Enumeration slots** -- lists like ``(`click`, `swipe`, ... , or `wait_for_delay`)``
are re-rendered from the actually-available tool set, so an absent tool never
appears. With the full tool set the rendering reproduces the original wording
byte-for-byte (ordering, backticks, Oxford ``or``/``and``), which is what the
regression tests pin.
* **Instruction segments** -- self-contained blocks that teach one tool (e.g. the
``manage_app`` app-launching rule) are included only while that tool exists.
An unavailable tool therefore leaves no trace in the prompt at all -- the executable
definition of "an unimplemented tool must cost the model nothing".
"""
from collections.abc import Iterable, Sequence, Set
__all__ = ["gate_segment", "render_tool_enum", "resolve_available"]
def render_tool_enum(
tools: Sequence[str],
available: Set[str],
final_sep: str | None = None,
) -> str:
"""Renders an ordered, backticked tool enumeration limited to available tools.
Args:
tools: Tool names in the exact order the prompt historically used.
available: Tools that currently exist.
final_sep: ``"or"``/``"and"`` to join the last item Oxford-style
(``"`a`, `b`, or `c`"``); ``None`` for a plain comma list.
Returns:
The rendered enumeration, or ``""`` when none of the tools are available.
"""
ticked = [f"`{name}`" for name in tools if name in available]
if not ticked:
return ""
if len(ticked) == 1:
return ticked[0]
if final_sep is None:
return ", ".join(ticked)
if len(ticked) == 2:
return f"{ticked[0]} {final_sep} {ticked[1]}"
return ", ".join(ticked[:-1]) + f", {final_sep} " + ticked[-1]
def gate_segment(segment: str, available: Set[str], *tools: str) -> str:
"""Returns ``segment`` when every tool it teaches is available, else ``""``."""
if all(name in available for name in tools):
return segment
return ""
def resolve_available(available: Iterable[str] | None, full_set: Set[str]) -> Set[str]:
"""Normalizes an optional availability argument.
``None`` means "everything" -- callers that have no actuator wired yet keep
today's output unchanged. Passing an explicit set opts into assembly.
"""
if available is None:
return full_set
return frozenset(available)