quash-mcp 0.2.12__py3-none-any.whl → 0.2.13__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.

Potentially problematic release.


This version of quash-mcp might be problematic. Click here for more details.

@@ -8,6 +8,10 @@ import httpx
8
8
  from typing import Dict, Any, Optional
9
9
  import logging
10
10
 
11
+ from .models import SessionDTO
12
+
13
+ from .models import SessionDTO
14
+
11
15
  logger = logging.getLogger(__name__)
12
16
 
13
17
 
@@ -192,61 +196,18 @@ class BackendClient:
192
196
 
193
197
  async def execute_step(
194
198
  self,
195
- api_key: str,
196
- session_id: str,
197
- step_number: int,
198
- task: str,
199
- ui_state: Dict[str, Any],
200
- chat_history: list,
201
- config: Dict[str, Any],
199
+ session: "SessionDTO",
202
200
  screenshot_bytes: Optional[bytes] = None
203
201
  ) -> Dict[str, Any]:
204
202
  """
205
- Execute single agent step (V3 - Step-by-step execution).
206
-
207
- Sends device state to backend, receives next action to execute.
208
-
209
- Args:
210
- api_key: Quash API key
211
- session_id: Unique session identifier
212
- step_number: Current step number
213
- task: Original task description
214
- ui_state: Device UI state (a11y_tree, phone_state)
215
- chat_history: Previous conversation messages
216
- config: Execution configuration
217
- screenshot_bytes: Optional screenshot (only if vision=True)
218
-
219
- Returns:
220
- Dict with action to execute:
221
- {
222
- "action": {"type": str, "code": str, "reasoning": str},
223
- "completed": bool,
224
- "success": bool (if completed),
225
- "final_message": str (if completed),
226
- "assistant_response": str,
227
- "tokens_used": {"prompt": int, "completion": int, "total": int},
228
- "cost": float
229
- }
203
+ Execute single agent step (V3 - DTO-based execution).
230
204
  """
231
205
  import json
232
206
 
233
207
  try:
234
208
  # Prepare form data (multipart)
235
- data_dict = {
236
- "api_key": api_key,
237
- "session_id": session_id,
238
- "step_number": step_number,
239
- "task": task,
240
- "ui_state": ui_state,
241
- "chat_history": chat_history,
242
- "config": config
243
- }
244
-
245
- # Convert to JSON string
246
- data_json = json.dumps(data_dict)
247
-
248
- # Prepare form data (data field as string)
249
- form_data = {"data": data_json}
209
+ session_json = session.model_dump_json()
210
+ form_data = {"session_data": session_json}
250
211
 
251
212
  # Prepare files dict (only screenshot if provided)
252
213
  files = {}
@@ -254,7 +215,6 @@ class BackendClient:
254
215
  files["screenshot"] = ("screenshot.png", screenshot_bytes, "image/png")
255
216
 
256
217
  async with httpx.AsyncClient(timeout=self.timeout) as client:
257
- # Send both form data and files (multipart/form-data)
258
218
  response = await client.post(
259
219
  f"{self.base_url}/api/agent/step",
260
220
  data=form_data,
@@ -283,60 +243,18 @@ class BackendClient:
283
243
 
284
244
  async def finalize_session(
285
245
  self,
286
- api_key: str,
287
- session_id: str,
288
- task: str,
289
- device_serial: str,
290
- status: str,
291
- final_message: Optional[str] = None,
292
- error: Optional[str] = None,
293
- duration_seconds: float = 0.0,
294
- config: Optional[Dict[str, Any]] = None
246
+ session: "SessionDTO",
295
247
  ) -> Dict[str, Any]:
296
248
  """
297
249
  Finalize a session and aggregate execution record.
298
-
299
- Called when task ends for ANY reason: normal completion, max steps, error, interrupt.
300
-
301
- Args:
302
- api_key: Quash API key
303
- session_id: Session identifier to finalize
304
- task: Original task description
305
- device_serial: Device serial number
306
- status: "success", "failed", "max_steps", "error", "interrupted"
307
- final_message: Final message from agent
308
- error: Error message if failed
309
- duration_seconds: Total execution time
310
- config: Execution configuration
311
-
312
- Returns:
313
- Dict with finalization result:
314
- {
315
- "finalized": bool,
316
- "execution_id": str,
317
- "total_steps": int,
318
- "total_tokens": {"prompt": int, "completion": int, "total": int},
319
- "total_cost": float,
320
- "error": str (if failed)
321
- }
322
250
  """
323
- logger.info(f"🏁 Finalizing session {session_id} - Status: {status}")
251
+ logger.info(f"🏁 Finalizing session {session.session_id}")
324
252
 
325
253
  try:
326
254
  async with httpx.AsyncClient(timeout=self.timeout) as client:
327
255
  response = await client.post(
328
256
  f"{self.base_url}/api/agent/finalize",
329
- json={
330
- "api_key": api_key,
331
- "session_id": session_id,
332
- "task": task,
333
- "device_serial": device_serial,
334
- "status": status,
335
- "final_message": final_message,
336
- "error": error,
337
- "duration_seconds": duration_seconds,
338
- "config": config or {}
339
- }
257
+ data={"session_data": session.model_dump_json(exclude={'ui_state', 'chat_history'})}
340
258
  )
341
259
 
342
260
  if response.status_code == 200:
@@ -345,8 +263,9 @@ class BackendClient:
345
263
  logger.info(f"✅ Session finalized: {result.get('total_steps')} steps, ${result.get('total_cost', 0):.4f}")
346
264
  return result
347
265
  else:
348
- logger.warning(f"Failed to finalize session: HTTP {response.status_code}")
349
- return {"finalized": False, "error": f"HTTP {response.status_code}"}
266
+ error_details = response.json() if response.headers.get("content-type") == "application/json" else response.text
267
+ logger.warning(f"Failed to finalize session: HTTP {response.status_code} - Details: {error_details}")
268
+ return {"finalized": False, "error": f"HTTP {response.status_code} - Details: {error_details}"}
350
269
 
351
270
  except Exception as e:
352
271
  logger.error(f"Failed to finalize session: {e}")
@@ -102,6 +102,48 @@ class AdbTools:
102
102
  logger.error(f"Failed to remove TCP port forwarding: {e}")
103
103
  return False
104
104
 
105
+ def get_accessibility_tree(self) -> str:
106
+ """
107
+ Get the current accessibility tree (UI hierarchy) as an XML string.
108
+ """
109
+ try:
110
+ # Use uiautomator dump to get the UI hierarchy
111
+ result = self.device.shell("uiautomator dump --compressed")
112
+ # The dump command writes to /sdcard/window_dump.xml
113
+ # We need to read it back
114
+ xml_content = self.device.pull("/sdcard/window_dump.xml").decode("utf-8")
115
+ # Clean up the dumped file from the device
116
+ self.device.shell("rm /sdcard/window_dump.xml")
117
+ return xml_content
118
+ except Exception as e:
119
+ logger.error(f"Failed to get accessibility tree: {e}", exc_info=True)
120
+ return f"<error>Failed to get accessibility tree: {e}</error>"
121
+
122
+ def get_phone_state(self) -> dict[str, any]:
123
+ """
124
+ Get basic phone state information, like current app package.
125
+ """
126
+ try:
127
+ current_app = self.device.current_app()
128
+ return {
129
+ "package": current_app.package,
130
+ "activity": current_app.activity,
131
+ "pid": current_app.pid,
132
+ }
133
+ except Exception as e:
134
+ logger.error(f"Failed to get phone state: {e}", exc_info=True)
135
+ return {"package": "unknown", "error": str(e)}
136
+
137
+ def get_screenshot(self) -> bytes:
138
+ """
139
+ Get a screenshot of the device as PNG bytes.
140
+ """
141
+ try:
142
+ return self.device.screenshot()
143
+ except Exception as e:
144
+ logger.error(f"Failed to get screenshot: {e}", exc_info=True)
145
+ return b""
146
+
105
147
  def __del__(self):
106
148
  """Cleanup when the object is destroyed."""
107
149
  if hasattr(self, "tcp_forwarded") and self.tcp_forwarded:
@@ -103,6 +103,11 @@ def get_device_state(serial: str) -> Tuple[Dict[str, Any], Optional[bytes]]:
103
103
  # Get current package
104
104
  current_package = get_current_package(serial)
105
105
 
106
+ logger.debug("Capturing device state...")
107
+
108
+ # Get current package
109
+ current_package = get_current_package(serial)
110
+
106
111
  # Get accessibility tree
107
112
  a11y_tree = get_accessibility_tree(serial)
108
113
 
quash_mcp/models.py ADDED
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List, Dict, Any
3
+ from datetime import datetime
4
+
5
+ class TokensInfo(BaseModel):
6
+ prompt: int
7
+ completion: int
8
+ total: int
9
+
10
+ class ConfigInfo(BaseModel):
11
+ model: str
12
+ temperature: float
13
+ vision: bool = False
14
+ reasoning: bool = False
15
+ reflection: bool = False
16
+ debug: bool = False
17
+
18
+ class UIStateInfo(BaseModel):
19
+ a11y_tree: str
20
+ phone_state: Dict[str, Any]
21
+
22
+ class ChatHistoryMessage(BaseModel):
23
+ role: str
24
+ content: str
25
+
26
+ class AgentStepDTO(BaseModel):
27
+ step_number: int
28
+ reasoning: Optional[str] = None
29
+ code: Optional[str] = None
30
+ tokens_used: TokensInfo
31
+ cost: float
32
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
33
+
34
+ class SessionDTO(BaseModel):
35
+ session_id: str
36
+ api_key: str
37
+ task: str
38
+ device_serial: str
39
+ config: ConfigInfo
40
+ chat_history: List[ChatHistoryMessage] = []
41
+ steps: List[AgentStepDTO] = []
42
+ ui_state: Optional[UIStateInfo] = None
@@ -149,7 +149,7 @@ def wait_for_action_effect(
149
149
  """
150
150
  # Check if action should change UI
151
151
  code_lower = executed_code.lower()
152
- if "get_state" in code_lower or "complete(" in code_lower:
152
+ if "get_state" in code_lower:
153
153
  # Action doesn't change UI - no need to wait
154
154
  time.sleep(0.1)
155
155
  return get_state_func(device_serial)[0], None, False
@@ -177,6 +177,8 @@ def wait_for_action_effect(
177
177
  # MAIN EXECUTION FUNCTION
178
178
  # ============================================================
179
179
 
180
+ from ..models import SessionDTO, UIStateInfo, ChatHistoryMessage, ConfigInfo, AgentStepDTO
181
+
180
182
  async def execute_v3(
181
183
  task: str,
182
184
  max_steps: int = 15,
@@ -184,25 +186,6 @@ async def execute_v3(
184
186
  ) -> Dict[str, Any]:
185
187
  """
186
188
  Execute automation task using step-by-step backend communication.
187
-
188
- Each step:
189
- 1. Capture device state (State A)
190
- 2. Send to backend for AI decision
191
- 3. Execute returned action locally
192
- 4. POLL until state changes (State B ≠ State A) or timeout
193
- 5. Send State B to backend in next iteration
194
- 6. Repeat until complete
195
-
196
- This ensures the backend always sees the UPDATED state after each action,
197
- preventing the agent from making decisions based on stale state.
198
-
199
- Args:
200
- task: Natural language task description
201
- max_steps: Maximum number of steps to execute (default: 15)
202
- progress_callback: Optional callback for progress updates
203
-
204
- Returns:
205
- Dict with execution result and details
206
189
  """
207
190
  state = get_state()
208
191
  backend = get_backend_client()
@@ -272,15 +255,18 @@ async def execute_v3(
272
255
  log_progress(f"🚀 Starting task: {task}")
273
256
  log_progress(f"📱 Device: {state.device_serial}")
274
257
  log_progress(f"🧠 Model: {config['model']}")
258
+
275
259
  log_progress(f"🔢 Max steps: {max_steps}")
276
260
 
277
- # Initialize execution
278
- start_time = time.time()
279
- session_id = f"session_{uuid.uuid4().hex[:12]}"
280
- step_number = 0
281
- chat_history = []
282
- total_tokens = {"prompt": 0, "completion": 0, "total": 0}
283
- total_cost = 0.0
261
+ # Initialize Session DTO
262
+
263
+ session = SessionDTO(
264
+ session_id=f"session_{uuid.uuid4().hex[:12]}",
265
+ api_key=quash_api_key,
266
+ task=task,
267
+ device_serial=state.device_serial,
268
+ config=ConfigInfo(**config)
269
+ )
284
270
 
285
271
  # Initialize local ADB tools for code execution
286
272
  adb_tools = AdbTools(serial=state.device_serial, use_tcp=True)
@@ -337,43 +323,48 @@ async def execute_v3(
337
323
 
338
324
  executor_locals = {}
339
325
 
326
+ start_time = time.time()
327
+
340
328
  try:
341
329
  # ============================================================
342
330
  # STEP-BY-STEP EXECUTION LOOP
343
331
  # ============================================================
344
- while step_number < max_steps: # Use user-provided max_steps
345
- step_number += 1
346
- log_progress(f"🧠 Step {step_number}/{max_steps}: Analyzing...")
347
332
 
348
- # 1. Capture device state (State A)
333
+ while len(session.steps) < max_steps:
334
+
335
+ log_progress(f"🧠 Step {len(session.steps) + 1}/{max_steps}: Analyzing...")
336
+
337
+ # 1. Capture device state and update session DTO
349
338
  try:
350
339
  ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
351
340
 
352
- # Only include screenshot if vision is enabled
341
+ session.ui_state = UIStateInfo(**ui_state_dict)
342
+ # Update local tools with new state
343
+ if mahoraga_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"], str):
344
+ try:
345
+ import json
346
+ a11y_tree_obj = json.loads(ui_state_dict["a11y_tree"])
347
+ mahoraga_tools.update_state(a11y_tree_obj)
348
+ except (json.JSONDecodeError, TypeError):
349
+ pass # Ignore if not a valid JSON string
350
+
353
351
  if not config["vision"]:
354
352
  screenshot_bytes = None
355
353
 
356
- # Log current state
357
354
  current_package = ui_state_dict.get("phone_state", {}).get("package", "unknown")
358
355
  log_progress(f"📱 Current app: {current_package}")
359
356
 
360
357
  except Exception as e:
361
358
  log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
362
- ui_state_dict = {
363
- "a11y_tree": [{"index": 0, "text": "Error capturing UI", "children": []}],
364
- "phone_state": {"package": "unknown"}
365
- }
359
+ session.ui_state = UIStateInfo(
360
+ a11y_tree="<error>Failed to capture UI</error>",
361
+ phone_state={"package": "unknown"}
362
+ )
366
363
  screenshot_bytes = None
367
364
 
368
- # 2. Send to backend for AI decision
365
+ # 2. Send session DTO to backend for AI decision
369
366
  step_result = await backend.execute_step(
370
- api_key=quash_api_key,
371
- session_id=session_id,
372
- step_number=step_number,
373
- task=task,
374
- ui_state=ui_state_dict,
375
- chat_history=chat_history,
376
- config=config,
367
+ session=session,
377
368
  screenshot_bytes=screenshot_bytes
378
369
  )
379
370
 
@@ -384,20 +375,20 @@ async def execute_v3(
384
375
  "status": "error",
385
376
  "message": step_result["message"],
386
377
  "error": step_result["error"],
387
- "steps_taken": step_number,
388
- "tokens": total_tokens,
389
- "cost": total_cost,
378
+ "steps_taken": len(session.steps),
379
+ "tokens": None,
380
+ "cost": None,
390
381
  "duration_seconds": time.time() - start_time
391
382
  }
392
383
 
393
- # Update usage tracking
394
- step_tokens = step_result.get("tokens_used", {})
395
- step_cost = step_result.get("cost", 0.0)
384
+ # Update Session DTO with new step and chat history
385
+ new_step_data = step_result.get("new_step")
386
+ if new_step_data:
387
+ new_step = AgentStepDTO(**new_step_data)
388
+ session.steps.append(new_step)
389
+ assistant_response = step_result.get("assistant_response", "")
390
+ session.chat_history.append(ChatHistoryMessage(role="assistant", content=assistant_response))
396
391
 
397
- total_tokens["prompt"] += step_tokens.get("prompt", 0)
398
- total_tokens["completion"] += step_tokens.get("completion", 0)
399
- total_tokens["total"] += step_tokens.get("total", 0)
400
- total_cost += step_cost
401
392
 
402
393
  # Get action from backend
403
394
  action = step_result.get("action", {})
@@ -405,15 +396,13 @@ async def execute_v3(
405
396
  code = action.get("code")
406
397
  reasoning = action.get("reasoning")
407
398
 
399
+
408
400
  # Log reasoning
409
401
  if reasoning:
410
402
  log_progress(f"🤔 Reasoning: {reasoning}")
411
403
 
412
- # Update chat history
413
- assistant_response = step_result.get("assistant_response", "")
414
- chat_history.append({"role": "assistant", "content": assistant_response})
415
404
 
416
- # 3. Check if task is complete
405
+ # 3. Check if task is complete BEFORE executing action
417
406
  if step_result.get("completed", False):
418
407
  success = step_result.get("success", False)
419
408
  final_message = step_result.get("final_message", "Task completed")
@@ -421,62 +410,64 @@ async def execute_v3(
421
410
  duration = time.time() - start_time
422
411
 
423
412
  if success:
424
- log_progress(f"✅ Task completed successfully in {step_number} steps")
425
- log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
413
+ log_progress(f"✅ Task completed successfully!")
414
+ else:
415
+ log_progress(f"❌ Task marked as failed")
416
+
417
+ # Finalize session on backend
418
+ finalize_result = await backend.finalize_session(session=session)
419
+
420
+ if success:
421
+ log_progress(f"✅ Task completed successfully in {len(session.steps)} steps")
422
+ log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
426
423
 
427
424
  return {
428
425
  "status": "success",
429
- "steps_taken": step_number,
426
+ "steps_taken": len(session.steps),
430
427
  "final_message": final_message,
431
428
  "message": f"✅ Success: {final_message}",
432
- "tokens": total_tokens,
433
- "cost": total_cost,
429
+ "tokens": finalize_result.get("total_tokens"),
430
+ "cost": finalize_result.get("total_cost"),
434
431
  "duration_seconds": duration
435
432
  }
436
433
  else:
437
434
  log_progress(f"❌ Task failed: {final_message}")
438
- log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
435
+ log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
439
436
 
440
437
  return {
441
438
  "status": "failed",
442
- "steps_taken": step_number,
439
+ "steps_taken": len(session.steps),
443
440
  "final_message": final_message,
444
441
  "message": f"❌ Failed: {final_message}",
445
- "tokens": total_tokens,
446
- "cost": total_cost,
442
+ "tokens": finalize_result.get("total_tokens"),
443
+ "cost": finalize_result.get("total_cost"),
447
444
  "duration_seconds": duration
448
445
  }
449
446
 
450
- # 4. Execute action locally
447
+
448
+ # 4. Execute action locally (only if task is not complete)
451
449
  if code and action_type == "execute_code":
452
450
  log_progress(f"⚡ Executing action...")
453
451
 
454
- # Store old UI state for comparison
455
- old_ui_state = ui_state_dict.copy()
452
+ log_progress(f"```python\n{code}\n```") # Log the code
453
+
454
+ old_ui_state = session.ui_state.model_dump().copy()
456
455
 
457
456
  try:
458
457
  import io
459
458
  import contextlib
460
459
 
461
- # Capture stdout and stderr to get tool function outputs
462
460
  stdout = io.StringIO()
463
461
  stderr = io.StringIO()
464
462
 
465
463
  with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
466
- # Execute code in sandbox
467
464
  exec(code, executor_globals, executor_locals)
468
465
 
469
- # Get captured output
470
466
  execution_output = stdout.getvalue()
471
467
  error_output = stderr.getvalue()
472
468
 
473
- # ============================================================
474
- # CRITICAL: Wait for state change (polling-based event detection)
475
- # ============================================================
476
469
  log_progress(f"⏳ Waiting for UI state to update...")
477
-
478
470
  try:
479
- # Poll until state changes or timeout
480
471
  new_ui_state_dict, _, state_changed = wait_for_action_effect(
481
472
  get_device_state,
482
473
  state.device_serial,
@@ -486,7 +477,6 @@ async def execute_v3(
486
477
  poll_interval=0.5
487
478
  )
488
479
 
489
- # Log what happened
490
480
  if state_changed:
491
481
  old_pkg = old_ui_state.get("phone_state", {}).get("package", "")
492
482
  new_pkg = new_ui_state_dict.get("phone_state", {}).get("package", "")
@@ -495,6 +485,7 @@ async def execute_v3(
495
485
  log_progress(f"✅ State changed: App switched ({old_pkg} → {new_pkg})")
496
486
  else:
497
487
  log_progress(f"✅ State changed: UI updated")
488
+
498
489
  else:
499
490
  log_progress(f"⚠️ WARNING: State did NOT change after action (timeout)")
500
491
  log_progress(f" This might mean the action had no effect or took too long")
@@ -502,10 +493,8 @@ async def execute_v3(
502
493
  except Exception as e:
503
494
  log_progress(f"⚠️ Error during state change detection: {e}")
504
495
  state_changed = False
505
- # Fallback: Just wait a bit
506
496
  time.sleep(1.5)
507
497
 
508
- # Build feedback message
509
498
  feedback_parts = []
510
499
 
511
500
  if execution_output:
@@ -523,56 +512,36 @@ async def execute_v3(
523
512
 
524
513
  log_progress(f"✅ {feedback[:200]}")
525
514
 
526
- # Add execution result to chat history
527
- chat_history.append({
528
- "role": "user",
529
- "content": f"Execution Result:\n```\n{feedback}\n```"
530
- })
515
+ session.chat_history.append(ChatHistoryMessage(role="user", content=f"Execution Result:\n```\n{feedback}\n```"))
516
+
531
517
 
532
518
  except Exception as e:
533
519
  error_msg = f"Error during execution: {str(e)}"
534
520
  log_progress(f"💥 Action failed: {error_msg}")
535
521
 
536
- # Add error to chat history
537
- chat_history.append({
538
- "role": "user",
539
- "content": f"Execution Error:\n```\n{error_msg}\n```"
540
- })
522
+ session.chat_history.append(ChatHistoryMessage(role="user", content=f"Execution Error:\n```\n{error_msg}\n```"))
541
523
 
542
- else:
543
- # No code to execute
524
+ elif not code:
544
525
  log_progress("⚠️ No action code provided by backend")
545
- chat_history.append({
546
- "role": "user",
547
- "content": "No code was provided. Please provide code to execute."
548
- })
526
+ session.chat_history.append(ChatHistoryMessage(role="user", content="No code was provided. Please provide code to execute."))
527
+
528
+
549
529
 
550
530
  # Max steps reached
551
531
  log_progress(f"⚠️ Reached maximum steps ({max_steps})")
552
- log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
553
532
 
554
533
  duration = time.time() - start_time
555
534
 
556
- # Finalize session on backend to create execution record
557
- await backend.finalize_session(
558
- api_key=quash_api_key,
559
- session_id=session_id,
560
- task=task,
561
- device_serial=state.device_serial,
562
- status="max_steps",
563
- final_message=f"Reached maximum step limit of {max_steps}",
564
- error=None,
565
- duration_seconds=duration,
566
- config=config
567
- )
535
+ # Finalize session on backend
536
+ finalize_result = await backend.finalize_session(session=session)
568
537
 
569
538
  return {
570
539
  "status": "failed",
571
- "steps_taken": step_number,
540
+ "steps_taken": len(session.steps),
572
541
  "final_message": f"Reached maximum step limit of {max_steps}",
573
542
  "message": "❌ Failed: Maximum steps reached",
574
- "tokens": total_tokens,
575
- "cost": total_cost,
543
+ "tokens": finalize_result.get("total_tokens"),
544
+ "cost": finalize_result.get("total_cost"),
576
545
  "duration_seconds": duration
577
546
  }
578
547
 
@@ -581,24 +550,14 @@ async def execute_v3(
581
550
  duration = time.time() - start_time
582
551
 
583
552
  # Finalize session on backend
584
- await backend.finalize_session(
585
- api_key=quash_api_key,
586
- session_id=session_id,
587
- task=task,
588
- device_serial=state.device_serial,
589
- status="interrupted",
590
- final_message="Task interrupted by user",
591
- error=None,
592
- duration_seconds=duration,
593
- config=config
594
- )
553
+ finalize_result = await backend.finalize_session(session=session)
595
554
 
596
555
  return {
597
556
  "status": "interrupted",
598
557
  "message": "ℹ️ Task execution interrupted",
599
- "steps_taken": step_number,
600
- "tokens": total_tokens,
601
- "cost": total_cost,
558
+ "steps_taken": len(session.steps),
559
+ "tokens": finalize_result.get("total_tokens"),
560
+ "cost": finalize_result.get("total_cost"),
602
561
  "duration_seconds": duration
603
562
  }
604
563
 
@@ -608,25 +567,15 @@ async def execute_v3(
608
567
  duration = time.time() - start_time
609
568
 
610
569
  # Finalize session on backend
611
- await backend.finalize_session(
612
- api_key=quash_api_key,
613
- session_id=session_id,
614
- task=task,
615
- device_serial=state.device_serial,
616
- status="error",
617
- final_message=None,
618
- error=error_msg,
619
- duration_seconds=duration,
620
- config=config
621
- )
570
+ finalize_result = await backend.finalize_session(session=session)
622
571
 
623
572
  return {
624
573
  "status": "error",
625
574
  "message": f"💥 Execution error: {error_msg}",
626
575
  "error": error_msg,
627
- "steps_taken": step_number,
628
- "tokens": total_tokens,
629
- "cost": total_cost,
576
+ "steps_taken": len(session.steps),
577
+ "tokens": finalize_result.get("total_tokens"),
578
+ "cost": finalize_result.get("total_cost"),
630
579
  "duration_seconds": duration
631
580
  }
632
581
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quash-mcp
3
- Version: 0.2.12
3
+ Version: 0.2.13
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,12 +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=zm_rh7C6yx6Y62YZ32w_nTiff0h8i0CE0OZXLUiqZwE,12794
3
+ quash_mcp/backend_client.py,sha256=S7IF95GEUtPZJPK9Dy0Gbbv7EV6SexOuAX0sw-u5i6I,9926
4
+ quash_mcp/models.py,sha256=0S7uCZZRRtQuSwIwKTDKZnX2HRMYOBDoDcl7-b8Tzpk,1001
4
5
  quash_mcp/server.py,sha256=scUGnplxjsvyYLK2q6hrjl-5Chkdnat9pODDtLzsQFY,15519
5
6
  quash_mcp/state.py,sha256=Tnt795GnZcas-h62Y6KYyIZVopeoWPM0TbRwOeVFYj4,4394
6
7
  quash_mcp/device/__init__.py,sha256=6e8CtHolt-vJKPxZUU_Vsd6-QGqos9VrFykaLTT90rk,772
7
- quash_mcp/device/adb_tools.py,sha256=Q0HIZHLBswaD94Qa7AA2VtjY31EZ68l0YClJDmMjOBU,3778
8
+ quash_mcp/device/adb_tools.py,sha256=SsYnzGjG3XsfbiAHiC7PpgLdC149kRH-YkoXQZvxvWc,5439
8
9
  quash_mcp/device/portal.py,sha256=sDLJOruUwwNNxIDriiXB4vT0BZYILidgzVgdhHCEkDY,5241
9
- quash_mcp/device/state_capture.py,sha256=2aq7A-CwKRSbXZAob-JdoK6FfGfLJRB77tMYyo-O2Vo,3349
10
+ quash_mcp/device/state_capture.py,sha256=WUNewRlgi_A6L8usWsga-9iqMTroCuSaZ0OdY8EheU0,3473
10
11
  quash_mcp/tools/__init__.py,sha256=r4fMAjHDjHUbimRwYW7VYUDkQHs12UVsG_IBmWpeX9s,249
11
12
  quash_mcp/tools/build.py,sha256=M6tGXWrQNkdtCYYrK14gUaoufQvyoor_hNN0lBPSVHY,30321
12
13
  quash_mcp/tools/build_old.py,sha256=6M9gaqZ_dX4B7UFTxSMD8T1BX0zEwQUL7RJ8ItNfB54,6016
@@ -14,10 +15,10 @@ quash_mcp/tools/configure.py,sha256=cv4RTolu6qae-XzyACSJUDrALfd0gYC-XE5s66_zfNk,
14
15
  quash_mcp/tools/connect.py,sha256=Kc7RGRUgtd2sR_bv6U4CB4kWSaLfsDc5kBo9u4FEjzs,4799
15
16
  quash_mcp/tools/execute.py,sha256=kR3VzIl31Lek-js4Hgxs-S_ls4YwKnbqkt79KFbvFuM,909
16
17
  quash_mcp/tools/execute_v2_backup.py,sha256=waWnaD0dEVcOJgRBbqZo3HnxME1s6YUOn8aRbm4R3X4,6081
17
- quash_mcp/tools/execute_v3.py,sha256=8WHiEcVWUcAekh7MGaqpXKYGAzdilQ1HaNankMoAxHI,23506
18
+ quash_mcp/tools/execute_v3.py,sha256=_tm0pcgSeGchooUFpiv1sTHY7w0psrCy2lQ3IDGhwKo,21899
18
19
  quash_mcp/tools/runsuite.py,sha256=gohLk9FpN8v7F0a69fspqOqUexTcslpYf3qU-iIZZ3s,7220
19
20
  quash_mcp/tools/usage.py,sha256=g76A6FO36fThoyRFG7q92QmS3Kh1pIKOrhYOzUdIubA,1155
20
- quash_mcp-0.2.12.dist-info/METADATA,sha256=ymeZQAKXV5hQElUhEGeiM5HEyVG9seGjhl0q-Y87nYU,8424
21
- quash_mcp-0.2.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- quash_mcp-0.2.12.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
23
- quash_mcp-0.2.12.dist-info/RECORD,,
21
+ quash_mcp-0.2.13.dist-info/METADATA,sha256=EdlucJ9MZlAjuTOgQzA-GVGgTcR8Eqo0NfhG4Tfv6EQ,8424
22
+ quash_mcp-0.2.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ quash_mcp-0.2.13.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
24
+ quash_mcp-0.2.13.dist-info/RECORD,,