From dd8df339109c5ea86e620c76e18583257ee3c7a7 Mon Sep 17 00:00:00 2001 From: showtan001 <240788545@qq.com> Date: Sun, 30 Aug 2026 22:38:28 +0800 Subject: [PATCH] Log task planner conversations locally --- apps/device-host-agent/host_agent/app.py | 3 +- .../device-host-agent/host_agent/execution.py | 12 +++++-- runtime/ai_planner.py | 35 +++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/apps/device-host-agent/host_agent/app.py b/apps/device-host-agent/host_agent/app.py index 75fae71..4e53313 100644 --- a/apps/device-host-agent/host_agent/app.py +++ b/apps/device-host-agent/host_agent/app.py @@ -231,12 +231,14 @@ def create_application( "MCP token generated at %s", mcp_token_path ) mcp_busy_tracker = McpBusyTracker(ttl_seconds=20.0) + conversation_log = ConversationLogStore(resolved_config.conversation_log_path) if resolved_config.mode == "local" else None executor = AssignmentExecutor( create_execution_factories( resolved_manager, metadata_store=metadata_store, timeline=timeline, host_agent_config=resolved_config, + conversation_log=conversation_log, ), mcp_busy_tracker=mcp_busy_tracker, ) @@ -246,7 +248,6 @@ def create_application( status_tracker=status_tracker, ) planner_config = load_planner_config() - conversation_log = ConversationLogStore(resolved_config.conversation_log_path) if resolved_config.mode == "local" else None conversation_agent = ( ConversationAgent( config=planner_config, diff --git a/apps/device-host-agent/host_agent/execution.py b/apps/device-host-agent/host_agent/execution.py index 6c58ef3..8375ac4 100644 --- a/apps/device-host-agent/host_agent/execution.py +++ b/apps/device-host-agent/host_agent/execution.py @@ -3,6 +3,7 @@ from __future__ import annotations import os from collections.abc import Callable from dataclasses import dataclass, replace +from typing import Any from device.manager import DeviceManager from host_agent.cloud_planner_client import CloudProxyToolCallingClient @@ -35,6 +36,7 @@ def create_execution_factories( metadata_store: TaskMetadataStore | None = None, timeline: Timeline | None = None, host_agent_config: HostAgentConfig | None = None, + conversation_log: Any | None = None, ) -> ExecutionFactories: shared_workflow_store = workflow_store or WorkflowStore() resolved_host_agent_config = host_agent_config @@ -48,7 +50,10 @@ def create_execution_factories( ), metadata_store=metadata_store, timeline=timeline, - planner=_host_agent_planner(resolved_host_agent_config), + planner=_host_agent_planner( + resolved_host_agent_config, + event_logger=conversation_log.append if conversation_log is not None else None, + ), planner_config=_host_agent_planner_config(), device_platform_provider=lambda device_id: _device_platform( manager, device_id @@ -86,6 +91,8 @@ def _host_agent_planner_config() -> PlannerConfig: def _host_agent_planner( host_agent_config: HostAgentConfig | None, + *, + event_logger: Callable[[dict[str, Any]], None] | None = None, ) -> Planner | None: """Build the `AIPlanner` explicitly when the cloud-proxy transport is selected, so its `ToolCallingClient` is a `CloudProxyToolCallingClient` @@ -100,11 +107,12 @@ def _host_agent_planner( resolved_config = host_agent_config or load_host_agent_config() if resolved_config.ai_planner_transport != "cloud": - return None + return AIPlanner(config=planner_config, event_logger=event_logger) return AIPlanner( client=CloudProxyToolCallingClient(resolved_config), config=planner_config, + event_logger=event_logger, ) diff --git a/runtime/ai_planner.py b/runtime/ai_planner.py index 961300d..136cca0 100644 --- a/runtime/ai_planner.py +++ b/runtime/ai_planner.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable from typing import TYPE_CHECKING, Any from core.errors import TaskFailedError @@ -23,9 +24,11 @@ class AIPlanner(Planner): *, client: ToolCallingClient | None = None, config: PlannerConfig | None = None, + event_logger: Callable[[dict[str, Any]], None] | None = None, ) -> None: self.config = config or load_config() self.client = client or build_client(self.config) + self.event_logger = event_logger def plan( self, @@ -49,13 +52,24 @@ class AIPlanner(Planner): "corrected action or finish the task if it cannot proceed.\n" f"Previous failure: {context.step_results[-1].error or 'unknown error'}" ) - decision = self.client.decide( - system_prompt=PLANNER_SYSTEM_PROMPT, - user_prompt=user_prompt, - screenshot=screenshot, - tools=ALL_TOOL_SPECS, - timeout=self.config.timeout, - ) + self._log({"type": "llm_request", "task_id": context.task_id, "goal": goal, + "system_prompt": PLANNER_SYSTEM_PROMPT, "user_prompt": user_prompt, + "has_screenshot": screenshot is not None}) + try: + decision = self.client.decide( + system_prompt=PLANNER_SYSTEM_PROMPT, + user_prompt=user_prompt, + screenshot=screenshot, + tools=ALL_TOOL_SPECS, + timeout=self.config.timeout, + ) + except Exception as exc: + self._log({"type": "agent_error", "task_id": context.task_id, "error": str(exc)}) + raise + self._log({"type": "llm_response", "task_id": context.task_id, + "content": decision.text_output, "thinking": decision.thinking, + "tool_name": decision.tool_name, "arguments": decision.arguments, + "purpose": decision.purpose, "expected_outcome": decision.expected_outcome}) if decision.tool_name == FINISH_TASK_TOOL: if decision.arguments.get("success"): @@ -85,6 +99,13 @@ class AIPlanner(Planner): # (mapped to an empty plan above), never via this hook. return False + def _log(self, event: dict[str, Any]) -> None: + if self.event_logger is not None: + try: + self.event_logger(event) + except Exception: + pass + def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]: if world is None: