echotools 2.3.49__tar.gz → 2.3.51__tar.gz

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.
Files changed (225) hide show
  1. {echotools-2.3.49 → echotools-2.3.51}/PKG-INFO +1 -1
  2. {echotools-2.3.49 → echotools-2.3.51}/pyproject.toml +1 -1
  3. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/stream.py +139 -22
  4. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history.py +0 -19
  5. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history_format.py +2 -5
  6. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -18
  7. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/core.py +1 -1
  8. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/parse.py +141 -9
  9. {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/PKG-INFO +1 -1
  10. {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/SOURCES.txt +2 -0
  11. echotools-2.3.51/src/tests/test_entml_agent_output_formats.py +143 -0
  12. echotools-2.3.51/src/tests/test_entml_parser_comprehensive.py +656 -0
  13. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_fncall_stream_thinking.py +47 -12
  14. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_stream_realtime_parse.py +3 -3
  15. {echotools-2.3.49 → echotools-2.3.51}/README.md +0 -0
  16. {echotools-2.3.49 → echotools-2.3.51}/docs/README.md +0 -0
  17. {echotools-2.3.49 → echotools-2.3.51}/setup.cfg +0 -0
  18. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/__init__.py +0 -0
  19. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/__init__.py +0 -0
  20. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/list_cache.py +0 -0
  21. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/memory_cache.py +0 -0
  22. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/__init__.py +0 -0
  23. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/base.py +0 -0
  24. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center.py +0 -0
  25. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center_notify.py +0 -0
  26. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center_template.py +0 -0
  27. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/loader.py +0 -0
  28. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/merge.py +0 -0
  29. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/__init__.py +0 -0
  30. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/base.py +0 -0
  31. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/classify.py +0 -0
  32. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/common.py +0 -0
  33. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/http.py +0 -0
  34. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/ids/__init__.py +0 -0
  35. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/ids/generator.py +0 -0
  36. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/__init__.py +0 -0
  37. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/io_utils.py +0 -0
  38. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/printstream.py +0 -0
  39. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/logger/__init__.py +0 -0
  40. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/logger/manager.py +0 -0
  41. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/retry/__init__.py +0 -0
  42. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/retry/retry.py +0 -0
  43. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/compat.py +0 -0
  44. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/__init__.py +0 -0
  45. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/candidate.py +0 -0
  46. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  47. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  48. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/race.py +0 -0
  49. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/selector.py +0 -0
  50. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/usage.py +0 -0
  51. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/__init__.py +0 -0
  52. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  53. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  54. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  55. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  56. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  57. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  58. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  59. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  60. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  61. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  62. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  63. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  64. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  65. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  66. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  67. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/registry.py +0 -0
  68. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  69. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  70. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  71. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  72. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  73. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  74. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/keys.py +0 -0
  75. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/__init__.py +0 -0
  76. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/manager.py +0 -0
  77. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater.py +0 -0
  78. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  79. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/process/__init__.py +0 -0
  80. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/process/port.py +0 -0
  81. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/protocol/__init__.py +0 -0
  82. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/protocol/base.py +0 -0
  83. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/__init__.py +0 -0
  84. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/conpty.py +0 -0
  85. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/__init__.py +0 -0
  86. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/core.py +0 -0
  87. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/proc.py +0 -0
  88. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/unix.py +0 -0
  89. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/win.py +0 -0
  90. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/sanitize.py +0 -0
  91. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/__init__.py +0 -0
  92. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/base.py +0 -0
  93. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/client.py +0 -0
  94. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/ssh.py +0 -0
  95. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/tmux.py +0 -0
  96. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/__init__.py +0 -0
  97. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/charmap.py +0 -0
  98. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/spinner.py +0 -0
  99. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/ui.py +0 -0
  100. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  101. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  102. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  103. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  104. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  105. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  106. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_console.py +0 -0
  107. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_io.py +0 -0
  108. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_log.py +0 -0
  109. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  110. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_text.py +0 -0
  111. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_types.py +0 -0
  112. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  113. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  114. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  115. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  116. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  117. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  118. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  119. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  120. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  121. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  122. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  123. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/__init__.py +0 -0
  124. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/bus.py +0 -0
  125. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/event.py +0 -0
  126. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/__init__.py +0 -0
  127. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/spinner.py +0 -0
  128. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/spinner_color.py +0 -0
  129. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/__init__.py +0 -0
  130. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/context.py +0 -0
  131. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/span.py +0 -0
  132. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/tracer.py +0 -0
  133. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/translate.py +0 -0
  134. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/watcher/__init__.py +0 -0
  135. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/watcher/file_watcher.py +0 -0
  136. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/__init__.py +0 -0
  137. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/application.py +0 -0
  138. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/broker.py +0 -0
  139. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/app.js +0 -0
  140. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  141. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  142. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  143. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  144. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  145. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  146. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  147. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  148. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  149. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  150. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.css +0 -0
  151. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.js +0 -0
  152. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/style.css +0 -0
  153. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/voicegif.js +0 -0
  154. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/middleware.py +0 -0
  155. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/stats.py +0 -0
  156. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/utils.py +0 -0
  157. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/files/__init__.py +0 -0
  158. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/files/file_util.py +0 -0
  159. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/network/__init__.py +0 -0
  160. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/network/http_utils.py +0 -0
  161. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/__init__.py +0 -0
  162. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/base.py +0 -0
  163. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/discovery.py +0 -0
  164. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/registry.py +0 -0
  165. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/registry_ops.py +0 -0
  166. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/proxy/__init__.py +0 -0
  167. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/proxy/manager.py +0 -0
  168. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/runtime/__init__.py +0 -0
  169. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/runtime/collector.py +0 -0
  170. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/scheduler/__init__.py +0 -0
  171. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/scheduler/scheduler.py +0 -0
  172. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/sdk/__init__.py +0 -0
  173. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/sdk/facade.py +0 -0
  174. {echotools-2.3.49 → echotools-2.3.51}/src/echotools/version.py +0 -0
  175. {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/dependency_links.txt +0 -0
  176. {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/requires.txt +0 -0
  177. {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/top_level.txt +0 -0
  178. {echotools-2.3.49 → echotools-2.3.51}/src/tests/bench_import.py +0 -0
  179. {echotools-2.3.49 → echotools-2.3.51}/src/tests/conftest.py +0 -0
  180. {echotools-2.3.49 → echotools-2.3.51}/src/tests/fixtures/__init__.py +0 -0
  181. {echotools-2.3.49 → echotools-2.3.51}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  182. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_adversarial_stream_splits.py +0 -0
  183. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_cache_retry.py +0 -0
  184. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_completion.py +0 -0
  185. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_config.py +0 -0
  186. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_config_extended.py +0 -0
  187. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_console.py +0 -0
  188. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_coverage_core.py +0 -0
  189. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch.py +0 -0
  190. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch_extended.py +0 -0
  191. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch_race.py +0 -0
  192. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_bare_invoke_format.py +0 -0
  193. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_protocol.py +0 -0
  194. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_stream_json.py +0 -0
  195. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  196. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_thinking_history.py +0 -0
  197. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_thinking_parse.py +0 -0
  198. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tool_parse_leak.py +0 -0
  199. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tool_pipeline.py +0 -0
  200. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tools.py +0 -0
  201. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_errors.py +0 -0
  202. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_errors_common.py +0 -0
  203. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_events.py +0 -0
  204. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_events_event.py +0 -0
  205. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_facade.py +0 -0
  206. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_fncall.py +0 -0
  207. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_ids.py +0 -0
  208. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_imports.py +0 -0
  209. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_io.py +0 -0
  210. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_keys.py +0 -0
  211. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_lifecycle.py +0 -0
  212. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_logger.py +0 -0
  213. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_merge.py +0 -0
  214. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_plugin.py +0 -0
  215. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_printstream.py +0 -0
  216. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_protocols.py +0 -0
  217. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_proxy.py +0 -0
  218. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_proxy_selector.py +0 -0
  219. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_runtime.py +0 -0
  220. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_scheduler.py +0 -0
  221. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  222. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_tracing.py +0 -0
  223. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_usage.py +0 -0
  224. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_web.py +0 -0
  225. {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_web_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.49
3
+ Version: 2.3.51
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.49"
7
+ version = "2.3.51"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -139,8 +139,25 @@ class FncallStreamParser:
139
139
  return buffer, ""
140
140
 
141
141
  def _append_content_text(self, text: str) -> None:
142
- if text:
143
- self._text_parts.append(text)
142
+ if not text:
143
+ return
144
+ from echotools.exec.fncall.protocols.entml_think.parse import _THINKING_CLOSE
145
+
146
+ remainder = text
147
+ while True:
148
+ stripped = remainder.strip()
149
+ if not stripped:
150
+ return
151
+ if stripped == _THINKING_CLOSE:
152
+ return
153
+ if _THINKING_CLOSE in remainder:
154
+ before, _, after = remainder.partition(_THINKING_CLOSE)
155
+ if before.strip():
156
+ self._text_parts.append(before)
157
+ remainder = after
158
+ continue
159
+ self._text_parts.append(remainder)
160
+ return
144
161
 
145
162
  def _normalize_stream_chunk(self, text: str) -> str:
146
163
  fn = getattr(self._protocol, "normalize_stream_buffer", None)
@@ -148,11 +165,64 @@ class FncallStreamParser:
148
165
  return fn(text)
149
166
  return text
150
167
 
168
+ def _invoke_close_tag(self) -> str:
169
+ return self._end_tags[0] if self._end_tags else "</entml:invoke>"
170
+
171
+ def _drain_thinking_holdback_to_fncall(self) -> None:
172
+ """invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
173
+ if self._thinking_filter is None:
174
+ return
175
+ pending = self._thinking_filter._pending
176
+ if pending:
177
+ self._fncall_buf += pending
178
+ self._thinking_filter._pending = ""
179
+
180
+ def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
181
+ self._fncall_buf = buffer_from[pos:]
182
+ self._drain_thinking_holdback_to_fncall()
183
+ self._detected = True
184
+ self._state = self.IN_FUNCTION_CALLS
185
+ self._json_stream_encoder = None
186
+ if self._thinking_filter is not None:
187
+ self._thinking_filter.clear_invoke_handoff_state()
188
+
151
189
  def _feed_content_waiting(self, text: str) -> None:
152
190
  """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
153
191
  if not text:
154
192
  return
155
193
  text = self._normalize_stream_chunk(text)
194
+ if self._state == self.IN_FUNCTION_CALLS:
195
+ close_tag = self._invoke_close_tag()
196
+ if not self._is_call_closed():
197
+ if close_tag in text:
198
+ idx = text.find(close_tag)
199
+ self._fncall_buf += text[: idx + len(close_tag)]
200
+ tail = text[idx + len(close_tag) :]
201
+ if tail:
202
+ self._feed_content_waiting(tail)
203
+ return
204
+ self._fncall_buf += text
205
+ return
206
+ trigger_tags = self._protocol.get_trigger_tags()
207
+ found, pos = self._protocol.detect_start(text)
208
+ if found:
209
+ if pos > 0:
210
+ self._append_content_text(text[:pos])
211
+ self._fncall_buf += text[pos:]
212
+ return
213
+ hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
214
+ if hold_from_fn is not None:
215
+ hold_from = hold_from_fn(text)
216
+ if hold_from is not None:
217
+ if hold_from > 0:
218
+ self._append_content_text(text[:hold_from])
219
+ self._waiting_tail = text[hold_from:]
220
+ return
221
+ safe, remain = self._split_safe_text(text, trigger_tags)
222
+ if safe:
223
+ self._append_content_text(safe)
224
+ self._waiting_tail = remain
225
+ return
156
226
  trigger_tags = self._protocol.get_trigger_tags()
157
227
  found, pos = self._protocol.detect_start(text)
158
228
  if not found:
@@ -173,11 +243,7 @@ class FncallStreamParser:
173
243
  if pos > 0:
174
244
  self._append_content_text(text[:pos])
175
245
 
176
- self._fncall_buf = text[pos:]
177
- self._detected = True
178
- self._state = self.IN_FUNCTION_CALLS
179
- if self._is_call_closed():
180
- self._state = self.DONE
246
+ self._begin_function_calls(text, pos=pos)
181
247
 
182
248
  def _feed_waiting_thinking_plain(self, combined: str) -> None:
183
249
  """未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
@@ -187,12 +253,28 @@ class FncallStreamParser:
187
253
  self._thinking_parts.append(part)
188
254
  elif part:
189
255
  self._feed_content_waiting(part)
256
+ if self._state == self.IN_FUNCTION_CALLS:
257
+ self._drain_thinking_holdback_to_fncall()
190
258
 
191
259
  def _feed_waiting(self, chunk: str) -> None:
192
- """在 WAITING_FOR_TAG 状态下处理新块。"""
260
+ """在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
193
261
  combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
194
262
  self._waiting_tail = ""
195
263
 
264
+ if self._state == self.IN_FUNCTION_CALLS:
265
+ if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
266
+ self._feed_waiting_thinking_plain(combined)
267
+ return
268
+ if self._waiting_tail:
269
+ self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
270
+ self._waiting_tail = ""
271
+ self._drain_thinking_holdback_to_fncall()
272
+ if not self._is_call_closed():
273
+ self._fncall_buf += combined
274
+ return
275
+ self._feed_content_waiting(combined)
276
+ return
277
+
196
278
  # 已进入 thinking 块时,块内不做 invoke 检测。
197
279
  if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
198
280
  self._feed_waiting_thinking_plain(combined)
@@ -202,14 +284,17 @@ class FncallStreamParser:
202
284
  found, pos = self._protocol.detect_start(combined)
203
285
 
204
286
  if found:
287
+ if self._thinking_filter is not None:
288
+ from echotools.exec.fncall.protocols.entml_think.parse import (
289
+ invoke_index_inside_unclosed_thinking,
290
+ )
291
+ if invoke_index_inside_unclosed_thinking(combined, pos):
292
+ self._feed_waiting_thinking_plain(combined)
293
+ return
205
294
  if pos > 0:
206
295
  # 工具标签前的正文仍可能含 thinking,走过滤器。
207
296
  self._emit_text(combined[:pos])
208
- self._fncall_buf = combined[pos:]
209
- self._detected = True
210
- self._state = self.IN_FUNCTION_CALLS
211
- if self._is_call_closed():
212
- self._state = self.DONE
297
+ self._begin_function_calls(combined, pos=pos)
213
298
  return
214
299
 
215
300
  # 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
@@ -261,25 +346,20 @@ class FncallStreamParser:
261
346
 
262
347
  self._raw_buf += chunk
263
348
 
264
- if self._state == self.WAITING_FOR_TAG:
349
+ if self._state != self.DONE:
265
350
  self._feed_waiting(chunk)
266
- else:
267
- self._fncall_buf += self._normalize_stream_chunk(chunk)
268
- if self._is_call_closed():
269
- self._state = self.DONE
270
351
 
271
352
  self._pending_stream_delta = self._poll_streaming_tool_input_delta()
272
353
  ready = self.get_ready_tool_calls()
273
- if ready:
274
- if self._json_stream_encoder is not None:
275
- self._json_stream_encoder.reset()
354
+ if ready and self._json_stream_encoder is not None:
355
+ self._json_stream_encoder.reset()
276
356
  return ready
277
357
 
278
358
  def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
279
359
  """invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
280
360
  if not self._detected or getattr(self._protocol, "id", None) != "entml":
281
361
  return None
282
- if self._state not in (self.IN_FUNCTION_CALLS, self.DONE):
362
+ if self._state not in (self.IN_FUNCTION_CALLS,):
283
363
  return None
284
364
  from echotools.exec.fncall.protocols.entml_stream_json import (
285
365
  EntmlInvokeJsonStreamEncoder,
@@ -304,6 +384,43 @@ class FncallStreamParser:
304
384
  self._pending_stream_delta = None
305
385
  return delta
306
386
 
387
+ @property
388
+ def streaming_invoke_closed(self) -> bool:
389
+ """流式 fncall 缓冲中是否已出现 ``</entml:invoke>``。"""
390
+ if getattr(self._protocol, "id", None) != "entml":
391
+ return False
392
+ from echotools.exec.fncall.protocols.entml_stream_json import _INVOKE_CLOSE
393
+
394
+ return _INVOKE_CLOSE in self._fncall_buf
395
+
396
+ def complete_stream_delta_if_needed(self) -> Optional[Tuple[str, str]]:
397
+ """invoke 已开标签但未闭合时,补齐 partial_json 尾部的合法 JSON 后缀。"""
398
+ if not self._detected or getattr(self._protocol, "id", None) != "entml":
399
+ return None
400
+ if self.streaming_invoke_closed:
401
+ return None
402
+ from echotools.exec.fncall.protocols.entml_stream_json import (
403
+ _INVOKE_CLOSE,
404
+ EntmlInvokeJsonStreamEncoder,
405
+ build_streaming_json_snapshot,
406
+ split_invoke_open,
407
+ )
408
+
409
+ parsed = split_invoke_open(self._fncall_buf)
410
+ if not parsed:
411
+ return None
412
+ name, body_start = parsed
413
+ body = self._fncall_buf[body_start:]
414
+ snapshot = build_streaming_json_snapshot(body + _INVOKE_CLOSE)
415
+ if not snapshot:
416
+ return None
417
+ if self._json_stream_encoder is None:
418
+ self._json_stream_encoder = EntmlInvokeJsonStreamEncoder()
419
+ delta = self._json_stream_encoder.poll(body + _INVOKE_CLOSE)
420
+ if not delta:
421
+ return None
422
+ return (name, delta)
423
+
307
424
  def _assembly_for_tool_parse(self) -> str:
308
425
  """可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
309
426
  return "".join(self._text_parts) + self._waiting_tail + self._fncall_buf
@@ -319,22 +319,3 @@ def _normalize_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
319
319
  return inject_orphan_tool_results(step2)
320
320
 
321
321
 
322
- # ---------------------------------------------------------------------------
323
- # 对话历史格式化
324
- # ---------------------------------------------------------------------------
325
-
326
-
327
- def _make_assistant_dedup_key(
328
- content: Optional[str],
329
- tool_calls: List[Dict[str, Any]],
330
- ) -> Tuple[str, Tuple[Tuple[str, str], ...]]:
331
- """生成 assistant 消息的去重键。"""
332
- safe_content = content or ""
333
- tc_key: Tuple[Tuple[str, str], ...] = tuple(
334
- (
335
- (tc.get("function") or {}).get("name") or "",
336
- (tc.get("function") or {}).get("arguments") or "",
337
- )
338
- for tc in tool_calls
339
- )
340
- return (safe_content, tc_key)
@@ -41,7 +41,6 @@ def _append_assistant_part(
41
41
  index: int,
42
42
  protocol: Optional[Any],
43
43
  call_id_to_name: Dict[str, str],
44
- seen_assistant_keys: set,
45
44
  *,
46
45
  include_thinking_in_history: bool,
47
46
  ) -> int:
@@ -55,7 +54,7 @@ def _append_assistant_part(
55
54
  tool_msgs.append(messages[j])
56
55
  j += 1
57
56
  blocks = format_assistant_block_with_results(
58
- m, tool_msgs, protocol, call_id_to_name, seen_assistant_keys,
57
+ m, tool_msgs, protocol, call_id_to_name,
59
58
  include_thinking_in_history=include_thinking_in_history,
60
59
  )
61
60
  for block in blocks:
@@ -63,7 +62,7 @@ def _append_assistant_part(
63
62
  return j
64
63
 
65
64
  blocks = format_assistant_block(
66
- m, content_str, protocol, call_id_to_name, seen_assistant_keys,
65
+ m, content_str, protocol, call_id_to_name,
67
66
  include_thinking_in_history=include_thinking_in_history,
68
67
  )
69
68
  for block in blocks:
@@ -81,7 +80,6 @@ def _format_conversation_history(
81
80
  return ""
82
81
 
83
82
  call_id_to_name: Dict[str, str] = {}
84
- seen_assistant_keys = set()
85
83
  parts: List[Tuple[str, bool]] = []
86
84
  clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
87
85
 
@@ -98,7 +96,6 @@ def _format_conversation_history(
98
96
  if role == "assistant":
99
97
  i = _append_assistant_part(
100
98
  parts, messages, i, protocol, call_id_to_name,
101
- seen_assistant_keys,
102
99
  include_thinking_in_history=include_thinking_in_history,
103
100
  )
104
101
  continue
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, Dict, List, Optional, Set, Tuple
4
4
 
5
- from echotools.base.logger.manager import get_logger
6
5
  from echotools.exec.fncall.protocols.entml_think.hist import (
7
6
  extract_reasoning_text,
8
7
  format_entml_thinking_history_block,
@@ -12,15 +11,12 @@ from echotools.exec.fncall.shared.normalization import normalize_content
12
11
  from .history import (
13
12
  _TOOL_CALL_MARKER_RE,
14
13
  _convert_assistant_pseudo_calls,
15
- _make_assistant_dedup_key,
16
14
  _render_inline_call_result,
17
15
  _render_tool_call,
18
16
  _render_tool_result,
19
17
  _try_convert_user_to_tool,
20
18
  )
21
19
 
22
- logger = get_logger(__name__)
23
-
24
20
 
25
21
  def _assistant_history_content_blocks(
26
22
  m: Dict[str, Any],
@@ -139,19 +135,12 @@ def format_assistant_block(
139
135
  content_str: str,
140
136
  protocol: Optional[Any],
141
137
  call_id_to_name: Dict[str, str],
142
- seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
143
138
  *,
144
139
  include_thinking_in_history: bool = False,
145
140
  ) -> List[str]:
146
141
  tcs: List[Dict[str, Any]] = m.get("tool_calls") or []
147
142
  _register_tool_call_names(tcs, call_id_to_name)
148
143
 
149
- dedup_key = _make_assistant_dedup_key(content_str, tcs)
150
- if dedup_key in seen_assistant_keys:
151
- logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
152
- return []
153
- seen_assistant_keys.add(dedup_key)
154
-
155
144
  parts: List[str] = []
156
145
  assistant = _format_assistant_text_block(
157
146
  m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
@@ -174,7 +163,6 @@ def format_assistant_block_with_results(
174
163
  tool_msgs: List[Dict[str, Any]],
175
164
  protocol: Optional[Any],
176
165
  call_id_to_name: Dict[str, str],
177
- seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
178
166
  *,
179
167
  include_thinking_in_history: bool = False,
180
168
  ) -> List[str]:
@@ -182,12 +170,6 @@ def format_assistant_block_with_results(
182
170
  content_str = normalize_content(m.get("content", ""))
183
171
  _register_tool_call_names(tcs, call_id_to_name)
184
172
 
185
- dedup_key = _make_assistant_dedup_key(content_str, tcs)
186
- if dedup_key in seen_assistant_keys:
187
- logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
188
- return []
189
- seen_assistant_keys.add(dedup_key)
190
-
191
173
  parts: List[str] = []
192
174
  assistant = _format_assistant_text_block(
193
175
  m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
@@ -65,7 +65,7 @@ _LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
65
65
  _THINKING_BEHAVIOR_ON_WITH_TOOLS = """\
66
66
  Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
67
67
 
68
- You MUST output a <entml:thinking> block before any other content in every reply. Only after that block, output your visible reply and/or <entml:invoke> tool call(s). Never skip the thinking block."""
68
+ You MUST output a <entml:thinking> block before any other content in every reply. Only after that block closes with </entml:thinking>, output your visible reply and/or <entml:invoke> tool call(s). Never place <entml:invoke> inside <entml:thinking>. Never skip the thinking block."""
69
69
 
70
70
  _THINKING_BEHAVIOR_ON_NO_TOOLS = """\
71
71
  Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import List, Tuple
4
+ from typing import List, Optional, Tuple
5
5
 
6
6
  from echotools.exec.fncall.protocols.entml_patterns import (
7
+ BLOCK_RE,
7
8
  extract_attr_value,
8
9
  normalize_entml_name,
9
10
  )
@@ -26,6 +27,62 @@ _AMBIGUOUS_ENTML_PREFIXES = (
26
27
  _INVOKE_PREFIX = "<entml:invoke"
27
28
 
28
29
 
30
+ def _is_placeholder_invoke_name(name: str) -> bool:
31
+ """提示词占位符(如 ``$FUNCTION_NAME``)不算真实工具调用。"""
32
+ n = (name or "").strip()
33
+ return not n or "$" in n
34
+
35
+
36
+ def _invoke_name_from_block(block: str) -> Optional[str]:
37
+ prefix_len = len(_INVOKE_PREFIX)
38
+ pos = block.find(_INVOKE_PREFIX)
39
+ if pos < 0:
40
+ return None
41
+ gt = block.find(">", pos + prefix_len)
42
+ if gt < 0:
43
+ return None
44
+ attrs = block[pos + prefix_len : gt]
45
+ name = extract_attr_value(attrs, "name")
46
+ if not name:
47
+ return None
48
+ return normalize_entml_name(name)
49
+
50
+
51
+ def find_actionable_invoke_block(buffer: str) -> Optional[Tuple[str, str, str]]:
52
+ """返回首个非占位符的完整 invoke 块:``(before, block, after)``。"""
53
+ if not buffer:
54
+ return None
55
+ for match in BLOCK_RE.finditer(buffer):
56
+ block = match.group(0)
57
+ name = _invoke_name_from_block(block)
58
+ if not name or _is_placeholder_invoke_name(name):
59
+ continue
60
+ return buffer[: match.start()], block, buffer[match.end() :]
61
+ return None
62
+
63
+
64
+ def find_actionable_invoke_open(buffer: str) -> int:
65
+ """返回首个非占位符且已闭合 ``>`` 的 invoke 开标签下标;否则 -1。"""
66
+ if not buffer:
67
+ return -1
68
+ search_from = 0
69
+ prefix_len = len(_INVOKE_PREFIX)
70
+ while True:
71
+ pos = buffer.find(_INVOKE_PREFIX, search_from)
72
+ if pos < 0:
73
+ return -1
74
+ close = buffer.find(">", pos + prefix_len)
75
+ if close < 0:
76
+ return -1
77
+ attrs = buffer[pos + prefix_len : close]
78
+ name = extract_attr_value(attrs, "name")
79
+ if name:
80
+ name = normalize_entml_name(name)
81
+ if name and not _is_placeholder_invoke_name(name):
82
+ return pos
83
+ search_from = close + 1
84
+
85
+
29
86
  def find_complete_entml_invoke_open(buffer: str) -> int:
30
87
  """返回首个含 name 且已闭合 ``>`` 的 ``<entml:invoke`` 起始下标;否则 -1。"""
31
88
  if not buffer:
@@ -59,9 +116,12 @@ def find_ambiguous_entml_tool_prefix(buffer: str) -> int:
59
116
 
60
117
 
61
118
  def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
62
- """thinking 块内:尾部是工具 entml 标签真前缀时 hold,避免误当 thinking 吐出。"""
119
+ """thinking 块内:从最早工具 entml 前缀处 hold,避免 invoke 被当作 thinking 吐出。"""
63
120
  if not buffer:
64
121
  return "", ""
122
+ ambig = find_ambiguous_entml_tool_prefix(buffer)
123
+ if ambig >= 0:
124
+ return buffer[:ambig], buffer[ambig:]
65
125
  lt = buffer.rfind("<")
66
126
  if lt < 0:
67
127
  return buffer, ""
@@ -103,6 +163,25 @@ def has_unclosed_entml_thinking(text: str) -> bool:
103
163
  return False
104
164
 
105
165
 
166
+ def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
167
+ """``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
168
+
169
+ 已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
170
+ """
171
+ if invoke_at < 0:
172
+ return False
173
+ think_open = text.find(_THINKING_OPEN_PREFIX)
174
+ if think_open < 0 or invoke_at <= think_open:
175
+ return False
176
+ entml_close = text.find(_THINKING_CLOSE, think_open)
177
+ if entml_close >= 0:
178
+ return invoke_at < entml_close
179
+ fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
180
+ if fault_at >= 0:
181
+ return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
182
+ return True
183
+
184
+
106
185
  def split_entml_thinking(text: str) -> Tuple[str, str]:
107
186
  """从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
108
187
  if not text:
@@ -147,6 +226,13 @@ class EntmlThinkingStreamFilter:
147
226
  self._fault_watch = False
148
227
  self._fault_buffer = ""
149
228
 
229
+ def clear_invoke_handoff_state(self) -> None:
230
+ """invoke 已交给正文/工具解析后,结束 thinking / fault 观察状态。"""
231
+ self._in_block = False
232
+ self._fault_watch = False
233
+ self._fault_buffer = ""
234
+ self._thinking_started = False
235
+
150
236
  def in_open_thinking(self) -> bool:
151
237
  """思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
152
238
  if self._in_block or self._fault_watch:
@@ -200,14 +286,56 @@ class EntmlThinkingStreamFilter:
200
286
  return True
201
287
  return False
202
288
 
289
+ def _try_emit_actionable_invoke_from_in_block(
290
+ self, out: List[Tuple[str, str]]
291
+ ) -> bool:
292
+ """thinking 块内出现真实 invoke 时,在该处结束 thinking 并交给正文解析。"""
293
+ split = find_actionable_invoke_block(self._pending)
294
+ if split is not None:
295
+ before, invoke_block, after = split
296
+ if before:
297
+ emitted = self._emit_thinking_piece(before)
298
+ if emitted:
299
+ out.append(("thinking", emitted))
300
+ out.append(("content", invoke_block))
301
+ self._in_block = False
302
+ self._fault_watch = False
303
+ self._fault_buffer = ""
304
+ self._thinking_started = False
305
+ rest = after.lstrip()
306
+ if rest.startswith(_THINKING_CLOSE):
307
+ rest = rest[len(_THINKING_CLOSE) :].lstrip()
308
+ self._pending = rest
309
+ return True
310
+ return False
311
+
312
+ def _emit_piece_with_actionable_invokes(
313
+ self, piece: str, out: List[Tuple[str, str]]
314
+ ) -> str:
315
+ """从 thinking 正文中切出真实 invoke 块,返回剩余 thinking 文本。"""
316
+ while piece:
317
+ split = find_actionable_invoke_block(piece)
318
+ if split is None:
319
+ break
320
+ before, invoke_block, rest = split
321
+ if before:
322
+ emitted = self._emit_thinking_piece(before)
323
+ if emitted:
324
+ out.append(("thinking", emitted))
325
+ out.append(("content", invoke_block))
326
+ piece = rest
327
+ return piece
328
+
203
329
  def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
204
330
  """处理块内 pending;返回 False 表示应退出 feed 主循环。"""
205
331
  close_at = self._pending.find(_THINKING_CLOSE)
206
332
  if close_at >= 0:
207
333
  piece = self._pending[:close_at]
208
- emitted = self._emit_thinking_piece(piece)
209
- if emitted:
210
- out.append(("thinking", emitted))
334
+ piece = self._emit_piece_with_actionable_invokes(piece, out)
335
+ if piece:
336
+ emitted = self._emit_thinking_piece(piece)
337
+ if emitted:
338
+ out.append(("thinking", emitted))
211
339
  self._in_block = False
212
340
  self._fault_watch = False
213
341
  self._fault_buffer = ""
@@ -215,6 +343,9 @@ class EntmlThinkingStreamFilter:
215
343
  self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
216
344
  return True
217
345
 
346
+ if self._try_emit_actionable_invoke_from_in_block(out):
347
+ return True
348
+
218
349
  fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
219
350
  if fault_at >= 0:
220
351
  before = self._pending[:fault_at]
@@ -318,10 +449,11 @@ class EntmlThinkingStreamFilter:
318
449
  self._in_block = False
319
450
  self._thinking_started = False
320
451
  if self._in_block:
321
- emitted = self._emit_thinking_piece(self._pending)
322
- if emitted:
323
- out.append(("thinking", emitted))
324
- self._pending = ""
452
+ if not self._try_emit_actionable_invoke_from_in_block(out):
453
+ emitted = self._emit_thinking_piece(self._pending)
454
+ if emitted:
455
+ out.append(("thinking", emitted))
456
+ self._pending = ""
325
457
  self._in_block = False
326
458
  self._thinking_started = False
327
459
  if self._pending:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.49
3
+ Version: 2.3.51
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -182,7 +182,9 @@ src/tests/test_coverage_core.py
182
182
  src/tests/test_dispatch.py
183
183
  src/tests/test_dispatch_extended.py
184
184
  src/tests/test_dispatch_race.py
185
+ src/tests/test_entml_agent_output_formats.py
185
186
  src/tests/test_entml_bare_invoke_format.py
187
+ src/tests/test_entml_parser_comprehensive.py
186
188
  src/tests/test_entml_protocol.py
187
189
  src/tests/test_entml_stream_json.py
188
190
  src/tests/test_entml_stream_json_exhaustive.py