myagent-ai 1.15.33 → 1.15.35

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.
@@ -637,10 +637,49 @@ class MainAgent(BaseAgent):
637
637
  "response": truncate_str(parsed.response, 500),
638
638
  "parse_success": parsed.parse_success,
639
639
  "needs_correction": parsed.needs_correction,
640
+ "output_block_complete": parsed.output_block_complete,
640
641
  }},
641
642
  stream_callback,
642
643
  )
643
644
 
645
+ # Step 4.2: <output> 块完整性检查 — 不完整的块不输出,触发修正
646
+ if not parsed.output_block_complete:
647
+ logger.warning(
648
+ f"[{task_id}] <output> 块不完整(缺少 </output> 闭合标签),"
649
+ f"跳过本轮输出和工具执行"
650
+ )
651
+ if _xml_correction_retries < 1:
652
+ _xml_correction_retries += 1
653
+ correction_prompt = (
654
+ "你的输出缺少 </output> 闭合标签,XML块不完整,"
655
+ "解析器不会处理不完整的块。\n"
656
+ "请严格按照 <output>...</output> 格式重新输出,"
657
+ "确保所有标签正确闭合。\n\n"
658
+ f"你上一次的原始输出如下:\n{llm_raw}"
659
+ )
660
+ conversation_history.append(
661
+ Message(role="assistant", content=llm_raw)
662
+ )
663
+ conversation_history.append(
664
+ Message(role="user", content=correction_prompt)
665
+ )
666
+ await self._emit_v2_event(
667
+ "v2_reasoning",
668
+ {"content": "⚠️ 模型输出XML块不完整,正在自动修正..."},
669
+ stream_callback,
670
+ )
671
+ continue # 重新进入循环,让 LLM 重新生成
672
+ else:
673
+ # 已重试过,强制终止并提示用户
674
+ logger.warning(f"[{task_id}] XML块仍不完整且已重试,终止循环")
675
+ context.working_memory["final_response"] = "模型输出格式异常,请重新尝试。"
676
+ await self._emit_v2_event(
677
+ "v2_reasoning",
678
+ {"content": "模型输出格式异常,已自动终止。"},
679
+ stream_callback,
680
+ )
681
+ break
682
+
644
683
  # Step 4.5: 解析失败处理 — 回退给 LLM 修正或提取周边文本
645
684
  if not parsed.parse_success:
646
685
  # 即使解析失败,如果 regex fallback 提取到了工具调用,仍然继续执行
@@ -152,6 +152,7 @@ class ParsedOutput:
152
152
  raw_text: str = ""
153
153
  parse_success: bool = False
154
154
  needs_correction: bool = False
155
+ output_block_complete: bool = False # </output> 闭合标签是否存在
155
156
 
156
157
 
157
158
  # ---------------------------------------------------------------------------
@@ -191,7 +192,7 @@ def _canonical_tag(tag_name: str) -> str:
191
192
  return _ALIAS_TO_CANONICAL.get(lower, lower)
192
193
 
193
194
 
194
- def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None = None) -> str:
195
+ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None = None, *, conservative: bool = False) -> str:
195
196
  """Extract the text content of ``<tag_name>…</tag_name>`` from *text*.
196
197
 
197
198
  Fault-tolerant strategies tried in order:
@@ -226,6 +227,10 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
226
227
  if m:
227
228
  return m.group(1)
228
229
 
230
+ # Conservative mode: only extract properly closed tags, skip all fallbacks
231
+ if conservative:
232
+ return ""
233
+
229
234
  # Strategy 2: Unclosed — content runs until the next opening/closing
230
235
  # sibling tag or </output>.
231
236
  sibling_names = [t for t in stop_tags if t.lower() != tag_name.lower()]
@@ -270,6 +275,8 @@ def _extract_all_tag_blocks(
270
275
  text: str,
271
276
  tag_name: str,
272
277
  parent_close_tag: str | None = None,
278
+ *,
279
+ conservative: bool = False,
273
280
  ) -> List[str]:
274
281
  """Extract all ``<tag_name>…`` blocks from *text*.
275
282
 
@@ -293,6 +300,10 @@ def _extract_all_tag_blocks(
293
300
  if properly_closed:
294
301
  return properly_closed
295
302
 
303
+ # Conservative mode: only extract properly closed blocks
304
+ if conservative:
305
+ return []
306
+
296
307
  # Strategy 2: Split by <tag> openings — each segment is a block
297
308
  positions = [
298
309
  m.end() for m in re.finditer(rf"<{tag_esc}[^>]*>", text, re.IGNORECASE)
@@ -377,6 +388,22 @@ def _strip_outer_noise(text: str) -> str:
377
388
  return text
378
389
 
379
390
 
391
+ def is_output_block_complete(raw_text: str) -> bool:
392
+ """Check if *raw_text* contains a properly closed ``<output>...</output>`` block.
393
+
394
+ Returns:
395
+ True if both ``<output>`` and ``</output>`` tags are present.
396
+ False if neither tag, or only the opening tag, is found.
397
+ """
398
+ if not raw_text:
399
+ return False
400
+ open_m = re.search(r"<output[^>]*>", raw_text, re.IGNORECASE)
401
+ if open_m is None:
402
+ return False
403
+ close_m = re.search(r"</output\s*>", raw_text[open_m.end():], re.IGNORECASE)
404
+ return close_m is not None
405
+
406
+
380
407
  # ---------------------------------------------------------------------------
381
408
  # Core custom parser — NO xml.etree.ElementTree
382
409
  # ---------------------------------------------------------------------------
@@ -399,63 +426,73 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
399
426
  parsed.needs_correction = True
400
427
  return parsed
401
428
 
429
+ # ── Step 0: Check <output> block completeness ──
430
+ parsed.output_block_complete = is_output_block_complete(raw_text)
431
+ conservative = not parsed.output_block_complete
432
+
433
+ if conservative:
434
+ logger.info(
435
+ "XML <output> 块不完整(缺少 </output> 闭合标签),"
436
+ "启用保守解析模式(仅提取完整闭合的标签)"
437
+ )
438
+
402
439
  # ── Step 1: Strip non-XML noise (text before/after <output>) ──
403
440
  body = _strip_outer_noise(raw_text)
404
441
 
405
442
  # ── Step 2: Extract each known top-level tag ──
406
443
 
407
444
  # usersays_correct
408
- raw_val = _extract_tag_content(body, "usersays_correct")
445
+ raw_val = _extract_tag_content(body, "usersays_correct", conservative=conservative)
409
446
  parsed.usersays_correct = _safe_strip(raw_val)
410
447
 
411
448
  # task_plan
412
- raw_val = _extract_tag_content(body, "task_plan")
449
+ raw_val = _extract_tag_content(body, "task_plan", conservative=conservative)
413
450
  parsed.task_plan = _safe_strip(raw_val)
414
451
 
415
452
  # response
416
- raw_val = _extract_tag_content(body, "response")
453
+ raw_val = _extract_tag_content(body, "response", conservative=conservative)
417
454
  parsed.response = _safe_strip(raw_val)
418
455
 
419
456
  # recall
420
- raw_val = _extract_tag_content(body, "recall")
457
+ raw_val = _extract_tag_content(body, "recall", conservative=conservative)
421
458
  parsed.recall = _safe_strip(raw_val)
422
459
 
423
460
  # knowledge
424
- raw_val = _extract_tag_content(body, "knowledge")
461
+ raw_val = _extract_tag_content(body, "knowledge", conservative=conservative)
425
462
  parsed.knowledge = _safe_strip(raw_val)
426
463
 
427
464
  # askuser (also try alias ask_user)
428
- raw_val = _extract_tag_content(body, "askuser")
465
+ raw_val = _extract_tag_content(body, "askuser", conservative=conservative)
429
466
  if not raw_val.strip():
430
- raw_val = _extract_tag_content(body, "ask_user")
467
+ raw_val = _extract_tag_content(body, "ask_user", conservative=conservative)
431
468
  parsed.ask_user = _safe_strip(raw_val)
432
469
 
433
470
  # get_knowledge
434
- raw_val = _extract_tag_content(body, "get_knowledge")
471
+ raw_val = _extract_tag_content(body, "get_knowledge", conservative=conservative)
435
472
  parsed.get_knowledge = _safe_strip(raw_val)
436
473
 
437
474
  # finish
438
- raw_val = _extract_tag_content(body, "finish")
475
+ raw_val = _extract_tag_content(body, "finish", conservative=conservative)
439
476
  parsed.finish = _parse_bool(raw_val, False)
440
477
 
441
478
  # finish_reason
442
- raw_val = _extract_tag_content(body, "finish_reason")
479
+ raw_val = _extract_tag_content(body, "finish_reason", conservative=conservative)
443
480
  parsed.finish_reason = _safe_strip(raw_val)
444
481
 
445
482
  # next_step
446
- raw_val = _extract_tag_content(body, "next_step")
483
+ raw_val = _extract_tag_content(body, "next_step", conservative=conservative)
447
484
  parsed.next_step = _safe_strip(raw_val)
448
485
 
449
486
  # mainsubject [v1.15.8] 会话标题自动命名
450
- raw_val = _extract_tag_content(body, "mainsubject")
487
+ raw_val = _extract_tag_content(body, "mainsubject", conservative=conservative)
451
488
  parsed.mainsubject = _safe_strip(raw_val)
452
489
 
453
490
  # ── Step 3: Parse <remember> (may contain <type> and <content>) ──
454
- remember_raw = _extract_tag_content(body, "remember")
491
+ remember_raw = _extract_tag_content(body, "remember", conservative=conservative)
455
492
  if remember_raw.strip():
456
493
  # Try structured format: <type>global</type><content>...</content>
457
- type_val = _extract_tag_content(remember_raw, "type", REMEMBER_INNER_TAGS)
458
- content_val = _extract_tag_content(remember_raw, "content", REMEMBER_INNER_TAGS)
494
+ type_val = _extract_tag_content(remember_raw, "type", REMEMBER_INNER_TAGS, conservative=conservative)
495
+ content_val = _extract_tag_content(remember_raw, "content", REMEMBER_INNER_TAGS, conservative=conservative)
459
496
 
460
497
  if content_val.strip():
461
498
  mem_type = _safe_strip(type_val) or "session"
@@ -469,9 +506,9 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
469
506
  parsed.remember_type = "session"
470
507
 
471
508
  # ── Step 4: Parse <toolstocal> → list of tool dicts ──
472
- toolstocal_raw = _extract_tag_content(body, "toolstocal")
509
+ toolstocal_raw = _extract_tag_content(body, "toolstocal", conservative=conservative)
473
510
  if toolstocal_raw.strip():
474
- parsed.tools_to_call = _parse_toolstocal(toolstocal_raw)
511
+ parsed.tools_to_call = _parse_toolstocal(toolstocal_raw, conservative=conservative)
475
512
 
476
513
  # ── Step 5: Determine parse success ──
477
514
  has_content = bool(
@@ -516,31 +553,32 @@ def _custom_parse(raw_text: str) -> ParsedOutput:
516
553
  return parsed
517
554
 
518
555
 
519
- def _parse_toolstocal(toolstocal_content: str) -> List[Dict[str, Any]]:
556
+ def _parse_toolstocal(toolstocal_content: str, *, conservative: bool = False) -> List[Dict[str, Any]]:
520
557
  """Parse ``<toolstocal>`` body into a list of tool descriptors."""
521
558
  tools: List[Dict[str, Any]] = []
522
559
 
523
560
  tool_blocks = _extract_all_tag_blocks(
524
- toolstocal_content, "tool", parent_close_tag="</toolstocal>"
561
+ toolstocal_content, "tool", parent_close_tag="</toolstocal>",
562
+ conservative=conservative,
525
563
  )
526
564
 
527
565
  for block in tool_blocks:
528
566
  tool: Dict[str, Any] = {
529
567
  "beforecalltext": _safe_strip(
530
- _extract_tag_content(block, "beforecalltext", TOOL_INNER_TAGS)
568
+ _extract_tag_content(block, "beforecalltext", TOOL_INNER_TAGS, conservative=conservative)
531
569
  ),
532
570
  "toolname": _safe_strip(
533
- _extract_tag_content(block, "toolname", TOOL_INNER_TAGS)
571
+ _extract_tag_content(block, "toolname", TOOL_INNER_TAGS, conservative=conservative)
534
572
  ),
535
573
  "parms": _safe_strip(
536
- _extract_tag_content(block, "parms", TOOL_INNER_TAGS)
574
+ _extract_tag_content(block, "parms", TOOL_INNER_TAGS, conservative=conservative)
537
575
  ),
538
576
  "timeout": _parse_int(
539
- _extract_tag_content(block, "timeout", TOOL_INNER_TAGS),
577
+ _extract_tag_content(block, "timeout", TOOL_INNER_TAGS, conservative=conservative),
540
578
  _DEFAULT_TIMEOUT,
541
579
  ),
542
580
  "callback": _parse_bool(
543
- _extract_tag_content(block, "callback", TOOL_INNER_TAGS),
581
+ _extract_tag_content(block, "callback", TOOL_INNER_TAGS, conservative=conservative),
544
582
  _DEFAULT_CALLBACK,
545
583
  ),
546
584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.15.33",
3
+ "version": "1.15.35",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {