quash-mcp 0.2.14__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quash_mcp/backend_client.py +1 -1
- quash_mcp/device/state_capture.py +20 -14
- quash_mcp/models.py +12 -1
- quash_mcp/tools/execute_v3.py +160 -132
- {quash_mcp-0.2.14.dist-info → quash_mcp-0.3.0.dist-info}/METADATA +1 -1
- {quash_mcp-0.2.14.dist-info → quash_mcp-0.3.0.dist-info}/RECORD +8 -8
- {quash_mcp-0.2.14.dist-info → quash_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {quash_mcp-0.2.14.dist-info → quash_mcp-0.3.0.dist-info}/entry_points.txt +0 -0
quash_mcp/backend_client.py
CHANGED
|
@@ -212,7 +212,7 @@ class BackendClient:
|
|
|
212
212
|
|
|
213
213
|
# Prepare files dict (only screenshot if provided)
|
|
214
214
|
files = {}
|
|
215
|
-
if screenshot_bytes:
|
|
215
|
+
if screenshot_bytes and len(screenshot_bytes) > 0:
|
|
216
216
|
files["screenshot"] = ("screenshot.png", screenshot_bytes, "image/png")
|
|
217
217
|
|
|
218
218
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
Device state capture utilities.
|
|
3
|
-
Captures UI state and screenshots from Android devices.
|
|
4
|
-
"""
|
|
5
|
-
|
|
1
|
+
import json
|
|
6
2
|
import logging
|
|
7
3
|
import requests
|
|
8
|
-
from typing import Dict, Any, Optional, Tuple
|
|
4
|
+
from typing import Dict, Any, Optional, Tuple, List
|
|
9
5
|
from adbutils import adb
|
|
10
6
|
|
|
11
7
|
logger = logging.getLogger("quash-device")
|
|
@@ -34,7 +30,7 @@ def get_current_package(serial: str) -> str:
|
|
|
34
30
|
return "unknown"
|
|
35
31
|
|
|
36
32
|
|
|
37
|
-
def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
|
|
33
|
+
def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> List[Dict[str, Any]]:
|
|
38
34
|
"""
|
|
39
35
|
Get accessibility tree from Portal app via TCP.
|
|
40
36
|
|
|
@@ -43,7 +39,7 @@ def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
|
|
|
43
39
|
tcp_port: Local TCP port for Portal communication
|
|
44
40
|
|
|
45
41
|
Returns:
|
|
46
|
-
Accessibility tree
|
|
42
|
+
Accessibility tree as a list of dictionaries, or an empty list if failed
|
|
47
43
|
"""
|
|
48
44
|
try:
|
|
49
45
|
device = adb.device(serial)
|
|
@@ -55,20 +51,27 @@ def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
|
|
|
55
51
|
)
|
|
56
52
|
|
|
57
53
|
if response.status_code == 200:
|
|
58
|
-
# Portal returns JSON with status and data fields
|
|
59
54
|
data = response.json()
|
|
60
55
|
if data.get("status") == "success":
|
|
61
|
-
|
|
56
|
+
# The 'data' field should contain the JSON string of the a11y_tree
|
|
57
|
+
a11y_tree_json_str = data.get("data", "[]")
|
|
58
|
+
try:
|
|
59
|
+
parsed_tree = json.loads(a11y_tree_json_str)
|
|
60
|
+
logger.debug(f"get_accessibility_tree returning tree of length: {len(parsed_tree)}")
|
|
61
|
+
return parsed_tree
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
logger.warning(f"Failed to parse a11y_tree JSON string: {a11y_tree_json_str}")
|
|
64
|
+
return []
|
|
62
65
|
else:
|
|
63
66
|
logger.warning(f"Portal error: {data.get('error', 'Unknown error')}")
|
|
64
|
-
return
|
|
67
|
+
return []
|
|
65
68
|
else:
|
|
66
69
|
logger.warning(f"Failed to get accessibility tree: HTTP {response.status_code}")
|
|
67
|
-
return
|
|
70
|
+
return []
|
|
68
71
|
|
|
69
72
|
except Exception as e:
|
|
70
73
|
logger.warning(f"Failed to get accessibility tree: {e}")
|
|
71
|
-
return
|
|
74
|
+
return []
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
def capture_screenshot(serial: str) -> Optional[bytes]:
|
|
@@ -83,7 +86,10 @@ def capture_screenshot(serial: str) -> Optional[bytes]:
|
|
|
83
86
|
"""
|
|
84
87
|
try:
|
|
85
88
|
device = adb.device(serial)
|
|
86
|
-
|
|
89
|
+
# device.shell("screencap -p", stream=True) returns an AdbConnection object (file-like)
|
|
90
|
+
# We need to read the bytes from it.
|
|
91
|
+
with device.shell("screencap -p", stream=True) as conn:
|
|
92
|
+
screenshot_bytes = conn.read(1024 * 1024 * 10) # Read up to 10MB
|
|
87
93
|
return screenshot_bytes
|
|
88
94
|
except Exception as e:
|
|
89
95
|
logger.error(f"Failed to capture screenshot: {e}")
|
quash_mcp/models.py
CHANGED
|
@@ -16,7 +16,7 @@ class ConfigInfo(BaseModel):
|
|
|
16
16
|
debug: bool = False
|
|
17
17
|
|
|
18
18
|
class UIStateInfo(BaseModel):
|
|
19
|
-
a11y_tree: str
|
|
19
|
+
a11y_tree: List[Dict[str, Any]]
|
|
20
20
|
phone_state: Dict[str, Any]
|
|
21
21
|
|
|
22
22
|
class ChatHistoryMessage(BaseModel):
|
|
@@ -31,6 +31,12 @@ class AgentStepDTO(BaseModel):
|
|
|
31
31
|
cost: float
|
|
32
32
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
33
33
|
|
|
34
|
+
class ReflectionInfo(BaseModel):
|
|
35
|
+
"""Information about a reflection step."""
|
|
36
|
+
goal_achieved: bool
|
|
37
|
+
advice: Optional[str] = None
|
|
38
|
+
summary: Optional[str] = None
|
|
39
|
+
|
|
34
40
|
class SessionDTO(BaseModel):
|
|
35
41
|
session_id: str
|
|
36
42
|
api_key: str
|
|
@@ -39,4 +45,9 @@ class SessionDTO(BaseModel):
|
|
|
39
45
|
config: ConfigInfo
|
|
40
46
|
chat_history: List[ChatHistoryMessage] = []
|
|
41
47
|
steps: List[AgentStepDTO] = []
|
|
48
|
+
current_plan: Optional[List[str]] = None
|
|
49
|
+
current_task_index: int = 0
|
|
42
50
|
ui_state: Optional[UIStateInfo] = None
|
|
51
|
+
last_reflection: Optional[ReflectionInfo] = None # Store the last reflection for the session
|
|
52
|
+
|
|
53
|
+
last_action_completed: Optional[bool] = None
|
quash_mcp/tools/execute_v3.py
CHANGED
|
@@ -182,7 +182,6 @@ from ..models import SessionDTO, UIStateInfo, ChatHistoryMessage, ConfigInfo, Ag
|
|
|
182
182
|
|
|
183
183
|
async def execute_v3(
|
|
184
184
|
task: str,
|
|
185
|
-
max_steps: int = 15,
|
|
186
185
|
progress_callback: Optional[Callable[[str], None]] = None
|
|
187
186
|
) -> Dict[str, Any]:
|
|
188
187
|
"""
|
|
@@ -223,6 +222,7 @@ async def execute_v3(
|
|
|
223
222
|
"reflection": state.config["reflection"],
|
|
224
223
|
"debug": state.config["debug"]
|
|
225
224
|
}
|
|
225
|
+
max_steps = state.config.get("max_steps", 15)
|
|
226
226
|
|
|
227
227
|
# Validate API key
|
|
228
228
|
validation_result = await backend.validate_api_key(quash_api_key)
|
|
@@ -256,80 +256,73 @@ async def execute_v3(
|
|
|
256
256
|
log_progress(f"🚀 Starting task: {task}")
|
|
257
257
|
log_progress(f"📱 Device: {state.device_serial}")
|
|
258
258
|
log_progress(f"🧠 Model: {config['model']}")
|
|
259
|
-
|
|
260
259
|
log_progress(f"🔢 Max steps: {max_steps}")
|
|
261
260
|
|
|
262
261
|
# Initialize Session DTO
|
|
263
|
-
|
|
264
262
|
session = SessionDTO(
|
|
265
263
|
session_id=f"session_{uuid.uuid4().hex[:12]}",
|
|
266
264
|
api_key=quash_api_key,
|
|
267
265
|
task=task,
|
|
268
266
|
device_serial=state.device_serial,
|
|
269
|
-
config=ConfigInfo(**config)
|
|
267
|
+
config=ConfigInfo(**config),
|
|
268
|
+
last_action_completed=None # Explicitly initialize the new field
|
|
270
269
|
)
|
|
271
270
|
|
|
272
|
-
# Initialize
|
|
273
|
-
|
|
271
|
+
# Initialize a single, powerful ADB tools instance from Mahoraga
|
|
272
|
+
mahoraga_tools = None
|
|
273
|
+
try:
|
|
274
|
+
mahoraga_tools = MahoragaAdbTools(
|
|
275
|
+
serial=state.device_serial,
|
|
276
|
+
use_tcp=True,
|
|
277
|
+
remote_tcp_port=8080
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
log_progress(f"⚠️ CRITICAL: Failed to initialize MahoragaAdbTools: {e}")
|
|
281
|
+
return {
|
|
282
|
+
"status": "error",
|
|
283
|
+
"message": f"💥 Failed to initialize ADB tools: {e}",
|
|
284
|
+
}
|
|
274
285
|
|
|
275
286
|
# Code executor namespace - add tool functions so generated code can call them
|
|
276
287
|
executor_globals = {
|
|
277
288
|
"__builtins__": __builtins__,
|
|
278
|
-
"adb_tools": adb_tools
|
|
279
289
|
}
|
|
280
290
|
|
|
281
|
-
# Add tool functions to executor namespace
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# Add wrapped function to globals so code can call it directly
|
|
317
|
-
executor_globals[tool_name] = make_printing_wrapper(tool_function)
|
|
318
|
-
|
|
319
|
-
# Override the 'complete' function to be a no-op
|
|
320
|
-
# The backend already handles completion via the 'completed' flag
|
|
321
|
-
def complete_no_op(success=True, reason=""):
|
|
322
|
-
"""No-op wrapper for complete() - completion is handled by backend."""
|
|
323
|
-
print(f"complete() called: success={success}, reason='{reason}'")
|
|
324
|
-
return None
|
|
325
|
-
|
|
326
|
-
executor_globals['complete'] = complete_no_op
|
|
327
|
-
|
|
328
|
-
log_progress(f"🔧 Loaded {len(filtered_tools)} tool functions: {list(filtered_tools.keys())}")
|
|
329
|
-
except Exception as e:
|
|
330
|
-
log_progress(f"⚠️ Warning: Could not load tool functions: {e}")
|
|
331
|
-
import traceback
|
|
332
|
-
log_progress(f"Traceback: {traceback.format_exc()}")
|
|
291
|
+
# Add tool functions to executor namespace
|
|
292
|
+
try:
|
|
293
|
+
# Get all tool functions from the single mahoraga_tools instance
|
|
294
|
+
tool_list = describe_tools(mahoraga_tools, exclude_tools=None)
|
|
295
|
+
|
|
296
|
+
# Filter by allowed tools from DEFAULT persona
|
|
297
|
+
allowed_tool_names = DEFAULT.allowed_tools if hasattr(DEFAULT, 'allowed_tools') else []
|
|
298
|
+
filtered_tools = {name: func for name, func in tool_list.items() if name in allowed_tool_names}
|
|
299
|
+
|
|
300
|
+
# Add each tool function to executor globals with print wrapper
|
|
301
|
+
for tool_name, tool_function in filtered_tools.items():
|
|
302
|
+
# Convert async functions to sync if needed
|
|
303
|
+
if asyncio.iscoroutinefunction(tool_function):
|
|
304
|
+
if async_to_sync:
|
|
305
|
+
tool_function = async_to_sync(tool_function)
|
|
306
|
+
|
|
307
|
+
# Wrap tool function to print its return value
|
|
308
|
+
def make_printing_wrapper(func):
|
|
309
|
+
"""Wrap a tool function to print its return value."""
|
|
310
|
+
def wrapper(*args, **kwargs):
|
|
311
|
+
result = func(*args, **kwargs)
|
|
312
|
+
# Print the result so stdout captures it
|
|
313
|
+
if result is not None:
|
|
314
|
+
print(result)
|
|
315
|
+
return result
|
|
316
|
+
return wrapper
|
|
317
|
+
|
|
318
|
+
# Add wrapped function to globals so code can call it directly
|
|
319
|
+
executor_globals[tool_name] = make_printing_wrapper(tool_function)
|
|
320
|
+
|
|
321
|
+
log_progress(f"🔧 Loaded {len(filtered_tools)} tool functions: {list(filtered_tools.keys())}")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
log_progress(f"⚠️ Warning: Could not load tool functions: {e}")
|
|
324
|
+
import traceback
|
|
325
|
+
log_progress(f"Traceback: {traceback.format_exc()}")
|
|
333
326
|
|
|
334
327
|
executor_locals = {}
|
|
335
328
|
|
|
@@ -349,14 +342,14 @@ async def execute_v3(
|
|
|
349
342
|
ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
|
|
350
343
|
|
|
351
344
|
session.ui_state = UIStateInfo(**ui_state_dict)
|
|
345
|
+
|
|
352
346
|
# Update local tools with new state
|
|
353
|
-
if mahoraga_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"],
|
|
347
|
+
if mahoraga_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"], list):
|
|
354
348
|
try:
|
|
355
|
-
|
|
356
|
-
a11y_tree_obj = json.loads(ui_state_dict["a11y_tree"])
|
|
349
|
+
a11y_tree_obj = ui_state_dict["a11y_tree"]
|
|
357
350
|
mahoraga_tools.update_state(a11y_tree_obj)
|
|
358
|
-
except
|
|
359
|
-
|
|
351
|
+
except Exception as e:
|
|
352
|
+
log_progress(f"⚠️ Warning: Failed to update mahoraga_tools state: {e}")
|
|
360
353
|
|
|
361
354
|
if not config["vision"]:
|
|
362
355
|
screenshot_bytes = None
|
|
@@ -367,7 +360,7 @@ async def execute_v3(
|
|
|
367
360
|
except Exception as e:
|
|
368
361
|
log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
|
|
369
362
|
session.ui_state = UIStateInfo(
|
|
370
|
-
a11y_tree=
|
|
363
|
+
a11y_tree=[],
|
|
371
364
|
phone_state={"package": "unknown"}
|
|
372
365
|
)
|
|
373
366
|
screenshot_bytes = None
|
|
@@ -391,14 +384,29 @@ async def execute_v3(
|
|
|
391
384
|
"duration_seconds": time.time() - start_time
|
|
392
385
|
}
|
|
393
386
|
|
|
394
|
-
# Update
|
|
387
|
+
# CRITICAL: Update the client's session DTO with the one returned from the backend
|
|
388
|
+
updated_session_data = step_result.get("updated_session")
|
|
389
|
+
if updated_session_data:
|
|
390
|
+
# Ensure last_action_completed field exists
|
|
391
|
+
if "last_action_completed" not in updated_session_data:
|
|
392
|
+
updated_session_data["last_action_completed"] = None
|
|
393
|
+
session = SessionDTO(**updated_session_data)
|
|
394
|
+
else:
|
|
395
|
+
# Fallback: if updated_session not returned, update locally
|
|
396
|
+
new_step_data = step_result.get("new_step")
|
|
397
|
+
if new_step_data:
|
|
398
|
+
new_step = AgentStepDTO(**new_step_data)
|
|
399
|
+
session.steps.append(new_step)
|
|
400
|
+
assistant_response = step_result.get("assistant_response", "")
|
|
401
|
+
session.chat_history.append(ChatHistoryMessage(role="assistant", content=assistant_response))
|
|
402
|
+
|
|
403
|
+
# CRITICAL FIX: Handle plan generation responses (which have new_step=None)
|
|
404
|
+
# These don't create actual steps, just show the plan
|
|
395
405
|
new_step_data = step_result.get("new_step")
|
|
396
|
-
if new_step_data:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
session.chat_history.append(ChatHistoryMessage(role="assistant", content=assistant_response))
|
|
401
|
-
|
|
406
|
+
if new_step_data is None and not updated_session_data:
|
|
407
|
+
# Plan was generated but no step was added
|
|
408
|
+
# This is normal - plan is informational only
|
|
409
|
+
pass
|
|
402
410
|
|
|
403
411
|
# Get action from backend
|
|
404
412
|
action = step_result.get("action", {})
|
|
@@ -406,18 +414,18 @@ async def execute_v3(
|
|
|
406
414
|
code = action.get("code")
|
|
407
415
|
reasoning = action.get("reasoning")
|
|
408
416
|
|
|
409
|
-
|
|
410
417
|
# Log reasoning
|
|
411
418
|
if reasoning:
|
|
412
419
|
log_progress(f"🤔 Reasoning: {reasoning}")
|
|
413
420
|
|
|
421
|
+
# CRITICAL FIX: Reset completion flag before executing
|
|
422
|
+
session.last_action_completed = False
|
|
414
423
|
|
|
415
|
-
# 3. Execute action locally
|
|
416
|
-
|
|
417
|
-
if code and action_type == "execute_code":
|
|
418
|
-
log_progress(f"⚡ Executing action...")
|
|
424
|
+
# 3. Execute action locally (if provided)
|
|
425
|
+
if code and (action_type == "execute_code" or action_type == "complete"):
|
|
419
426
|
|
|
420
|
-
log_progress(f"
|
|
427
|
+
log_progress(f"⚡ Executing action...")
|
|
428
|
+
log_progress(f"```python\n{code}\n```")
|
|
421
429
|
|
|
422
430
|
old_ui_state = session.ui_state.model_dump().copy()
|
|
423
431
|
|
|
@@ -434,6 +442,13 @@ async def execute_v3(
|
|
|
434
442
|
execution_output = stdout.getvalue()
|
|
435
443
|
error_output = stderr.getvalue()
|
|
436
444
|
|
|
445
|
+
# CRITICAL FIX: Check if complete() was actually called
|
|
446
|
+
if mahoraga_tools and mahoraga_tools.finished:
|
|
447
|
+
log_progress("✅ Agent has signaled task completion via complete()")
|
|
448
|
+
session.last_action_completed = True
|
|
449
|
+
else:
|
|
450
|
+
session.last_action_completed = False
|
|
451
|
+
|
|
437
452
|
log_progress(f"⏳ Waiting for UI state to update...")
|
|
438
453
|
try:
|
|
439
454
|
new_ui_state_dict, _, state_changed = wait_for_action_effect(
|
|
@@ -453,7 +468,6 @@ async def execute_v3(
|
|
|
453
468
|
log_progress(f"✅ State changed: App switched ({old_pkg} → {new_pkg})")
|
|
454
469
|
else:
|
|
455
470
|
log_progress(f"✅ State changed: UI updated")
|
|
456
|
-
|
|
457
471
|
else:
|
|
458
472
|
log_progress(f"⚠️ WARNING: State did NOT change after action (timeout)")
|
|
459
473
|
log_progress(f" This might mean the action had no effect or took too long")
|
|
@@ -468,7 +482,10 @@ async def execute_v3(
|
|
|
468
482
|
if execution_output:
|
|
469
483
|
feedback_parts.append(f"Action output: {execution_output.strip()}")
|
|
470
484
|
|
|
471
|
-
|
|
485
|
+
# CRITICAL FIX: Report completion status in feedback
|
|
486
|
+
if session.last_action_completed:
|
|
487
|
+
feedback_parts.append("Sub-task completed successfully (complete() was called)")
|
|
488
|
+
elif state_changed:
|
|
472
489
|
feedback_parts.append("UI state updated successfully")
|
|
473
490
|
else:
|
|
474
491
|
feedback_parts.append("WARNING: UI state did not change (action may have failed)")
|
|
@@ -480,71 +497,86 @@ async def execute_v3(
|
|
|
480
497
|
|
|
481
498
|
log_progress(f"✅ {feedback[:200]}")
|
|
482
499
|
|
|
483
|
-
session.chat_history.append(ChatHistoryMessage(
|
|
500
|
+
session.chat_history.append(ChatHistoryMessage(
|
|
501
|
+
role="user",
|
|
502
|
+
content=f"Execution Result:\n```\n{feedback}\n```"
|
|
503
|
+
))
|
|
484
504
|
|
|
485
|
-
|
|
486
|
-
time.sleep(1.0) # Added delay
|
|
505
|
+
time.sleep(0.5)
|
|
487
506
|
|
|
488
507
|
except Exception as e:
|
|
489
508
|
error_msg = f"Error during execution: {str(e)}"
|
|
490
509
|
log_progress(f"💥 Action failed: {error_msg}")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
510
|
+
session.last_action_completed = False
|
|
511
|
+
|
|
512
|
+
session.chat_history.append(ChatHistoryMessage(
|
|
513
|
+
role="user",
|
|
514
|
+
content=f"Execution Error:\n```\n{error_output.strip()}\n```"
|
|
515
|
+
))
|
|
516
|
+
|
|
517
|
+
# 4. Check if overall task is complete
|
|
518
|
+
# CRITICAL FIX: In reasoning mode with planning, DON'T exit on first complete() call
|
|
519
|
+
# The backend controls when all tasks are done via the "complete" action type
|
|
520
|
+
should_exit = False
|
|
521
|
+
|
|
522
|
+
if mahoraga_tools and mahoraga_tools.finished:
|
|
523
|
+
# Check if this is the FINAL completion from the backend
|
|
524
|
+
# In reasoning mode, the backend returns action.type="complete" when ALL tasks are done
|
|
525
|
+
action_type = action.get("type", "")
|
|
526
|
+
|
|
527
|
+
if action_type == "complete":
|
|
528
|
+
# Backend explicitly says we're done with ALL tasks
|
|
529
|
+
should_exit = True
|
|
530
|
+
success = mahoraga_tools.success
|
|
531
|
+
final_message = mahoraga_tools.reason
|
|
532
|
+
elif config["reasoning"] and session.current_plan:
|
|
533
|
+
# In reasoning mode with a plan, a single complete() call is just for one sub-task
|
|
534
|
+
# Continue the loop - the backend will advance to the next task
|
|
535
|
+
log_progress(f"✅ Sub-task completed. Moving to next task...")
|
|
536
|
+
should_exit = False
|
|
537
|
+
else:
|
|
538
|
+
# Non-reasoning mode: first complete() means done
|
|
539
|
+
should_exit = True
|
|
540
|
+
success = mahoraga_tools.success
|
|
541
|
+
final_message = mahoraga_tools.reason
|
|
542
|
+
|
|
543
|
+
if should_exit and mahoraga_tools and mahoraga_tools.finished:
|
|
544
|
+
success = mahoraga_tools.success
|
|
545
|
+
final_message = mahoraga_tools.reason
|
|
504
546
|
duration = time.time() - start_time
|
|
505
547
|
|
|
506
548
|
if success:
|
|
507
549
|
log_progress(f"✅ Task completed successfully!")
|
|
508
550
|
else:
|
|
509
|
-
log_progress(f"❌ Task marked as failed")
|
|
551
|
+
log_progress(f"❌ Task marked as failed: {final_message}")
|
|
510
552
|
|
|
511
553
|
# Finalize session on backend
|
|
512
554
|
finalize_result = await backend.finalize_session(session=session)
|
|
555
|
+
total_tokens = finalize_result.get("total_tokens", {})
|
|
556
|
+
total_cost = finalize_result.get("total_cost", 0)
|
|
513
557
|
|
|
514
|
-
|
|
515
|
-
log_progress(f"✅ Task completed successfully in {len(session.steps)} steps")
|
|
516
|
-
log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
"status": "success",
|
|
520
|
-
"steps_taken": len(session.steps),
|
|
521
|
-
"final_message": final_message,
|
|
522
|
-
"message": f"✅ Success: {final_message}",
|
|
523
|
-
"tokens": finalize_result.get("total_tokens"),
|
|
524
|
-
"cost": finalize_result.get("total_cost"),
|
|
525
|
-
"duration_seconds": duration
|
|
526
|
-
}
|
|
527
|
-
else:
|
|
528
|
-
log_progress(f"❌ Task failed: {final_message}")
|
|
529
|
-
log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
|
|
530
|
-
|
|
531
|
-
return {
|
|
532
|
-
"status": "failed",
|
|
533
|
-
"steps_taken": len(session.steps),
|
|
534
|
-
"final_message": final_message,
|
|
535
|
-
"message": f"❌ Failed: {final_message}",
|
|
536
|
-
"tokens": finalize_result.get("total_tokens"),
|
|
537
|
-
"cost": finalize_result.get("total_cost"),
|
|
538
|
-
"duration_seconds": duration
|
|
539
|
-
}
|
|
558
|
+
log_progress(f"💰 Usage: {total_tokens.get('total')} tokens, ${total_cost:.4f}")
|
|
540
559
|
|
|
560
|
+
return {
|
|
561
|
+
"status": "success" if success else "failed",
|
|
562
|
+
"steps_taken": len(session.steps),
|
|
563
|
+
"final_message": final_message,
|
|
564
|
+
"message": f"✅ Success: {final_message}" if success else f"❌ Failed: {final_message}",
|
|
565
|
+
"tokens": total_tokens,
|
|
566
|
+
"cost": total_cost,
|
|
567
|
+
"duration_seconds": duration
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
elif not code:
|
|
571
|
+
log_progress("⚠️ No action code provided by backend")
|
|
572
|
+
session.chat_history.append(ChatHistoryMessage(
|
|
573
|
+
role="user",
|
|
574
|
+
content="No code was provided. Please provide code to execute."
|
|
575
|
+
))
|
|
541
576
|
|
|
542
577
|
# Max steps reached
|
|
543
578
|
log_progress(f"⚠️ Reached maximum steps ({max_steps})")
|
|
544
|
-
|
|
545
579
|
duration = time.time() - start_time
|
|
546
|
-
|
|
547
|
-
# Finalize session on backend
|
|
548
580
|
finalize_result = await backend.finalize_session(session=session)
|
|
549
581
|
|
|
550
582
|
return {
|
|
@@ -560,8 +592,6 @@ async def execute_v3(
|
|
|
560
592
|
except KeyboardInterrupt:
|
|
561
593
|
log_progress("ℹ️ Task interrupted by user")
|
|
562
594
|
duration = time.time() - start_time
|
|
563
|
-
|
|
564
|
-
# Finalize session on backend
|
|
565
595
|
finalize_result = await backend.finalize_session(session=session)
|
|
566
596
|
|
|
567
597
|
return {
|
|
@@ -577,8 +607,6 @@ async def execute_v3(
|
|
|
577
607
|
error_msg = str(e)
|
|
578
608
|
log_progress(f"💥 Error: {error_msg}")
|
|
579
609
|
duration = time.time() - start_time
|
|
580
|
-
|
|
581
|
-
# Finalize session on backend
|
|
582
610
|
finalize_result = await backend.finalize_session(session=session)
|
|
583
611
|
|
|
584
612
|
return {
|
|
@@ -593,5 +621,5 @@ async def execute_v3(
|
|
|
593
621
|
|
|
594
622
|
finally:
|
|
595
623
|
# Cleanup TCP forwarding
|
|
596
|
-
if
|
|
597
|
-
|
|
624
|
+
if mahoraga_tools:
|
|
625
|
+
mahoraga_tools.teardown_tcp_forward()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quash-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Model Context Protocol server for Quash - AI-powered mobile automation agent
|
|
5
5
|
Project-URL: Homepage, https://quashbugs.com
|
|
6
6
|
Project-URL: Repository, https://github.com/quash/quash-mcp
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
quash_mcp/__init__.py,sha256=LImiWCRgjAbb5DZXBq2DktUEAbftvnO61Vil4Ayun9A,39
|
|
2
2
|
quash_mcp/__main__.py,sha256=WCg5OlnXhr6i0XJHAUGpbhliMy3qE2SJkFzVD4wO-lw,239
|
|
3
|
-
quash_mcp/backend_client.py,sha256=
|
|
4
|
-
quash_mcp/models.py,sha256=
|
|
3
|
+
quash_mcp/backend_client.py,sha256=_smBbhyJxN1dj89cJNVvqRZVX92oc1bw2SI1vzX2Rek,9979
|
|
4
|
+
quash_mcp/models.py,sha256=zqi0-DCmgOaq4TiuJsb9QsQxMxcJ82B3NeRwbnrfJQc,1414
|
|
5
5
|
quash_mcp/server.py,sha256=scUGnplxjsvyYLK2q6hrjl-5Chkdnat9pODDtLzsQFY,15519
|
|
6
6
|
quash_mcp/state.py,sha256=Tnt795GnZcas-h62Y6KYyIZVopeoWPM0TbRwOeVFYj4,4394
|
|
7
7
|
quash_mcp/device/__init__.py,sha256=6e8CtHolt-vJKPxZUU_Vsd6-QGqos9VrFykaLTT90rk,772
|
|
8
8
|
quash_mcp/device/adb_tools.py,sha256=SsYnzGjG3XsfbiAHiC7PpgLdC149kRH-YkoXQZvxvWc,5439
|
|
9
9
|
quash_mcp/device/portal.py,sha256=sDLJOruUwwNNxIDriiXB4vT0BZYILidgzVgdhHCEkDY,5241
|
|
10
|
-
quash_mcp/device/state_capture.py,sha256=
|
|
10
|
+
quash_mcp/device/state_capture.py,sha256=NwuhjCBI576w9eexhdVOxfsOmABTW1A4SWRpcjadg-w,4016
|
|
11
11
|
quash_mcp/tools/__init__.py,sha256=r4fMAjHDjHUbimRwYW7VYUDkQHs12UVsG_IBmWpeX9s,249
|
|
12
12
|
quash_mcp/tools/build.py,sha256=M6tGXWrQNkdtCYYrK14gUaoufQvyoor_hNN0lBPSVHY,30321
|
|
13
13
|
quash_mcp/tools/build_old.py,sha256=6M9gaqZ_dX4B7UFTxSMD8T1BX0zEwQUL7RJ8ItNfB54,6016
|
|
@@ -15,10 +15,10 @@ quash_mcp/tools/configure.py,sha256=cv4RTolu6qae-XzyACSJUDrALfd0gYC-XE5s66_zfNk,
|
|
|
15
15
|
quash_mcp/tools/connect.py,sha256=Kc7RGRUgtd2sR_bv6U4CB4kWSaLfsDc5kBo9u4FEjzs,4799
|
|
16
16
|
quash_mcp/tools/execute.py,sha256=kR3VzIl31Lek-js4Hgxs-S_ls4YwKnbqkt79KFbvFuM,909
|
|
17
17
|
quash_mcp/tools/execute_v2_backup.py,sha256=waWnaD0dEVcOJgRBbqZo3HnxME1s6YUOn8aRbm4R3X4,6081
|
|
18
|
-
quash_mcp/tools/execute_v3.py,sha256=
|
|
18
|
+
quash_mcp/tools/execute_v3.py,sha256=KMS7Zru2GiDcp1IqDvtGPhKlzbXEilCRp8hkvZShI2Q,24404
|
|
19
19
|
quash_mcp/tools/runsuite.py,sha256=gohLk9FpN8v7F0a69fspqOqUexTcslpYf3qU-iIZZ3s,7220
|
|
20
20
|
quash_mcp/tools/usage.py,sha256=g76A6FO36fThoyRFG7q92QmS3Kh1pIKOrhYOzUdIubA,1155
|
|
21
|
-
quash_mcp-0.
|
|
22
|
-
quash_mcp-0.
|
|
23
|
-
quash_mcp-0.
|
|
24
|
-
quash_mcp-0.
|
|
21
|
+
quash_mcp-0.3.0.dist-info/METADATA,sha256=wLT-0D39eXubj1b8LiX57vBUae0ITz_1x1yCozxFUwM,8423
|
|
22
|
+
quash_mcp-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
quash_mcp-0.3.0.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
|
|
24
|
+
quash_mcp-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|