echotools 2.3.49__tar.gz → 2.3.50__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.50}/PKG-INFO +1 -1
  2. {echotools-2.3.49 → echotools-2.3.50}/pyproject.toml +1 -1
  3. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/stream.py +32 -10
  4. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history.py +0 -19
  5. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history_format.py +2 -5
  6. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -18
  7. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/parse.py +19 -0
  8. {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/PKG-INFO +1 -1
  9. {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/SOURCES.txt +2 -0
  10. echotools-2.3.50/src/tests/test_entml_agent_output_formats.py +143 -0
  11. echotools-2.3.50/src/tests/test_entml_parser_comprehensive.py +656 -0
  12. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_fncall_stream_thinking.py +38 -3
  13. {echotools-2.3.49 → echotools-2.3.50}/README.md +0 -0
  14. {echotools-2.3.49 → echotools-2.3.50}/docs/README.md +0 -0
  15. {echotools-2.3.49 → echotools-2.3.50}/setup.cfg +0 -0
  16. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/__init__.py +0 -0
  17. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/__init__.py +0 -0
  18. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/list_cache.py +0 -0
  19. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/memory_cache.py +0 -0
  20. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/__init__.py +0 -0
  21. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/base.py +0 -0
  22. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center.py +0 -0
  23. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center_notify.py +0 -0
  24. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center_template.py +0 -0
  25. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/loader.py +0 -0
  26. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/merge.py +0 -0
  27. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/__init__.py +0 -0
  28. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/base.py +0 -0
  29. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/classify.py +0 -0
  30. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/common.py +0 -0
  31. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/http.py +0 -0
  32. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/ids/__init__.py +0 -0
  33. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/ids/generator.py +0 -0
  34. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/__init__.py +0 -0
  35. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/io_utils.py +0 -0
  36. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/printstream.py +0 -0
  37. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/logger/__init__.py +0 -0
  38. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/logger/manager.py +0 -0
  39. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/retry/__init__.py +0 -0
  40. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/retry/retry.py +0 -0
  41. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/compat.py +0 -0
  42. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/__init__.py +0 -0
  43. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/candidate.py +0 -0
  44. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  45. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  46. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/race.py +0 -0
  47. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/selector.py +0 -0
  48. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/usage.py +0 -0
  49. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/__init__.py +0 -0
  50. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  51. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  52. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  53. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  54. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  55. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  56. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  57. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  58. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  59. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  60. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  61. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  62. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  63. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  64. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  65. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  66. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/registry.py +0 -0
  67. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  68. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  69. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  70. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  71. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  72. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  73. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/keys.py +0 -0
  74. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/__init__.py +0 -0
  75. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/manager.py +0 -0
  76. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater.py +0 -0
  77. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  78. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/process/__init__.py +0 -0
  79. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/process/port.py +0 -0
  80. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/protocol/__init__.py +0 -0
  81. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/protocol/base.py +0 -0
  82. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/__init__.py +0 -0
  83. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/conpty.py +0 -0
  84. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/__init__.py +0 -0
  85. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/core.py +0 -0
  86. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/proc.py +0 -0
  87. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/unix.py +0 -0
  88. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/win.py +0 -0
  89. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/sanitize.py +0 -0
  90. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/__init__.py +0 -0
  91. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/base.py +0 -0
  92. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/client.py +0 -0
  93. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/ssh.py +0 -0
  94. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/tmux.py +0 -0
  95. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/__init__.py +0 -0
  96. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/charmap.py +0 -0
  97. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/spinner.py +0 -0
  98. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/ui.py +0 -0
  99. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  100. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  101. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  102. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  103. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  104. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  105. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_console.py +0 -0
  106. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_io.py +0 -0
  107. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_log.py +0 -0
  108. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  109. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_text.py +0 -0
  110. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_types.py +0 -0
  111. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  112. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  113. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  114. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  115. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  116. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  117. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  118. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  119. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  120. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  121. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  122. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/__init__.py +0 -0
  123. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/bus.py +0 -0
  124. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/event.py +0 -0
  125. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/__init__.py +0 -0
  126. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/spinner.py +0 -0
  127. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/spinner_color.py +0 -0
  128. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/__init__.py +0 -0
  129. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/context.py +0 -0
  130. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/span.py +0 -0
  131. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/tracer.py +0 -0
  132. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/translate.py +0 -0
  133. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/watcher/__init__.py +0 -0
  134. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/watcher/file_watcher.py +0 -0
  135. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/__init__.py +0 -0
  136. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/application.py +0 -0
  137. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/broker.py +0 -0
  138. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/app.js +0 -0
  139. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  140. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  141. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  142. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  143. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  144. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  145. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  146. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  147. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  148. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  149. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.css +0 -0
  150. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.js +0 -0
  151. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/style.css +0 -0
  152. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/voicegif.js +0 -0
  153. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/middleware.py +0 -0
  154. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/stats.py +0 -0
  155. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/utils.py +0 -0
  156. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/files/__init__.py +0 -0
  157. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/files/file_util.py +0 -0
  158. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/network/__init__.py +0 -0
  159. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/network/http_utils.py +0 -0
  160. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/__init__.py +0 -0
  161. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/base.py +0 -0
  162. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/discovery.py +0 -0
  163. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/registry.py +0 -0
  164. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/registry_ops.py +0 -0
  165. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/proxy/__init__.py +0 -0
  166. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/proxy/manager.py +0 -0
  167. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/runtime/__init__.py +0 -0
  168. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/runtime/collector.py +0 -0
  169. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/scheduler/__init__.py +0 -0
  170. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/scheduler/scheduler.py +0 -0
  171. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/sdk/__init__.py +0 -0
  172. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/sdk/facade.py +0 -0
  173. {echotools-2.3.49 → echotools-2.3.50}/src/echotools/version.py +0 -0
  174. {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/dependency_links.txt +0 -0
  175. {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/requires.txt +0 -0
  176. {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/top_level.txt +0 -0
  177. {echotools-2.3.49 → echotools-2.3.50}/src/tests/bench_import.py +0 -0
  178. {echotools-2.3.49 → echotools-2.3.50}/src/tests/conftest.py +0 -0
  179. {echotools-2.3.49 → echotools-2.3.50}/src/tests/fixtures/__init__.py +0 -0
  180. {echotools-2.3.49 → echotools-2.3.50}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  181. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_adversarial_stream_splits.py +0 -0
  182. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_cache_retry.py +0 -0
  183. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_completion.py +0 -0
  184. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_config.py +0 -0
  185. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_config_extended.py +0 -0
  186. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_console.py +0 -0
  187. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_coverage_core.py +0 -0
  188. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch.py +0 -0
  189. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch_extended.py +0 -0
  190. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch_race.py +0 -0
  191. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_bare_invoke_format.py +0 -0
  192. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_protocol.py +0 -0
  193. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_stream_json.py +0 -0
  194. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  195. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_thinking_history.py +0 -0
  196. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_thinking_parse.py +0 -0
  197. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tool_parse_leak.py +0 -0
  198. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tool_pipeline.py +0 -0
  199. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tools.py +0 -0
  200. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_errors.py +0 -0
  201. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_errors_common.py +0 -0
  202. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_events.py +0 -0
  203. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_events_event.py +0 -0
  204. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_facade.py +0 -0
  205. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_fncall.py +0 -0
  206. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_ids.py +0 -0
  207. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_imports.py +0 -0
  208. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_io.py +0 -0
  209. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_keys.py +0 -0
  210. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_lifecycle.py +0 -0
  211. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_logger.py +0 -0
  212. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_merge.py +0 -0
  213. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_plugin.py +0 -0
  214. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_printstream.py +0 -0
  215. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_protocols.py +0 -0
  216. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_proxy.py +0 -0
  217. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_proxy_selector.py +0 -0
  218. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_runtime.py +0 -0
  219. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_scheduler.py +0 -0
  220. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  221. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_stream_realtime_parse.py +0 -0
  222. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_tracing.py +0 -0
  223. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_usage.py +0 -0
  224. {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_web.py +0 -0
  225. {echotools-2.3.49 → echotools-2.3.50}/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.50
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.50"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -148,6 +148,23 @@ class FncallStreamParser:
148
148
  return fn(text)
149
149
  return text
150
150
 
151
+ def _drain_thinking_holdback_to_fncall(self) -> None:
152
+ """invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
153
+ if self._thinking_filter is None:
154
+ return
155
+ pending = self._thinking_filter._pending
156
+ if pending:
157
+ self._fncall_buf += pending
158
+ self._thinking_filter._pending = ""
159
+
160
+ def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
161
+ self._fncall_buf = buffer_from[pos:]
162
+ self._drain_thinking_holdback_to_fncall()
163
+ self._detected = True
164
+ self._state = self.IN_FUNCTION_CALLS
165
+ if self._is_call_closed():
166
+ self._state = self.DONE
167
+
151
168
  def _feed_content_waiting(self, text: str) -> None:
152
169
  """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
153
170
  if not text:
@@ -173,11 +190,7 @@ class FncallStreamParser:
173
190
  if pos > 0:
174
191
  self._append_content_text(text[:pos])
175
192
 
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
193
+ self._begin_function_calls(text, pos=pos)
181
194
 
182
195
  def _feed_waiting_thinking_plain(self, combined: str) -> None:
183
196
  """未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
@@ -187,6 +200,8 @@ class FncallStreamParser:
187
200
  self._thinking_parts.append(part)
188
201
  elif part:
189
202
  self._feed_content_waiting(part)
203
+ if self._state == self.IN_FUNCTION_CALLS:
204
+ self._drain_thinking_holdback_to_fncall()
190
205
 
191
206
  def _feed_waiting(self, chunk: str) -> None:
192
207
  """在 WAITING_FOR_TAG 状态下处理新块。"""
@@ -202,14 +217,17 @@ class FncallStreamParser:
202
217
  found, pos = self._protocol.detect_start(combined)
203
218
 
204
219
  if found:
220
+ if self._thinking_filter is not None:
221
+ from echotools.exec.fncall.protocols.entml_think.parse import (
222
+ invoke_index_inside_unclosed_thinking,
223
+ )
224
+ if invoke_index_inside_unclosed_thinking(combined, pos):
225
+ self._feed_waiting_thinking_plain(combined)
226
+ return
205
227
  if pos > 0:
206
228
  # 工具标签前的正文仍可能含 thinking,走过滤器。
207
229
  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
230
+ self._begin_function_calls(combined, pos=pos)
213
231
  return
214
232
 
215
233
  # 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
@@ -264,6 +282,10 @@ class FncallStreamParser:
264
282
  if self._state == self.WAITING_FOR_TAG:
265
283
  self._feed_waiting(chunk)
266
284
  else:
285
+ if self._waiting_tail:
286
+ self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
287
+ self._waiting_tail = ""
288
+ self._drain_thinking_holdback_to_fncall()
267
289
  self._fncall_buf += self._normalize_stream_chunk(chunk)
268
290
  if self._is_call_closed():
269
291
  self._state = self.DONE
@@ -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
@@ -103,6 +103,25 @@ def has_unclosed_entml_thinking(text: str) -> bool:
103
103
  return False
104
104
 
105
105
 
106
+ def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
107
+ """``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
108
+
109
+ 已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
110
+ """
111
+ if invoke_at < 0:
112
+ return False
113
+ think_open = text.find(_THINKING_OPEN_PREFIX)
114
+ if think_open < 0 or invoke_at <= think_open:
115
+ return False
116
+ entml_close = text.find(_THINKING_CLOSE, think_open)
117
+ if entml_close >= 0:
118
+ return invoke_at < entml_close
119
+ fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
120
+ if fault_at >= 0:
121
+ return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
122
+ return True
123
+
124
+
106
125
  def split_entml_thinking(text: str) -> Tuple[str, str]:
107
126
  """从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
108
127
  if not text:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.49
3
+ Version: 2.3.50
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
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ """模型/agent 常见输出格式:正确 entml:invoke vs 错误 JSON/旧格式。"""
4
+
5
+ import json
6
+
7
+ import pytest
8
+
9
+ from echotools.exec.fncall import get_protocol
10
+ from echotools.exec.fncall.parsers.stream import FncallStreamParser
11
+
12
+ TOOLS = [
13
+ {
14
+ "type": "function",
15
+ "function": {
16
+ "name": "Read",
17
+ "description": "Read a file",
18
+ "parameters": {
19
+ "type": "object",
20
+ "properties": {"path": {"type": "string"}},
21
+ "required": ["path"],
22
+ },
23
+ },
24
+ },
25
+ {
26
+ "type": "function",
27
+ "function": {
28
+ "name": "get_time",
29
+ "description": "Get current time",
30
+ "parameters": {"type": "object", "properties": {}, "required": []},
31
+ },
32
+ },
33
+ ]
34
+
35
+ VALID_INVOKE = (
36
+ '<entml:invoke name="Read">\n'
37
+ '<entml:parameter name="path">C:/Users/Administrator/cursor_agent_simple.py</entml:parameter>\n'
38
+ "</entml:invoke>"
39
+ )
40
+
41
+ WRONG_JSON_TOOL = (
42
+ '{"name": "Read", "arguments": {"path": "C:/Users/Administrator/cursor_agent_simple.py"}}'
43
+ )
44
+
45
+ WRONG_TOOL_BLOCK = (
46
+ "<tool>\n"
47
+ '{"name": "Read", "arguments": {"path": "x.py"}}\n'
48
+ "</tool>"
49
+ )
50
+
51
+ FAKE_PARTIAL_JSON = '{"name": "Read", "arguments": {"path":'
52
+
53
+
54
+ def _parse_batch(text: str):
55
+ proto = get_protocol("entml")
56
+ clean, calls = proto.parse(text, TOOLS)
57
+ return clean, calls
58
+
59
+
60
+ def _parse_stream(text: str, *, chunk: int = 3):
61
+ proto = get_protocol("entml")
62
+ parser = FncallStreamParser(protocol=proto, tools=TOOLS)
63
+ for i in range(0, len(text), chunk):
64
+ parser.feed(text[i : i + chunk])
65
+ return parser.finalize()
66
+
67
+
68
+ @pytest.mark.parametrize("chunk", [1, 4, 17])
69
+ def test_valid_entml_invoke_batch_and_stream(chunk: int) -> None:
70
+ text = f"我先读文件。\n{VALID_INVOKE}\n完成。"
71
+ clean, calls = _parse_batch(text)
72
+ assert len(calls) == 1
73
+ assert calls[0]["function"]["name"] == "Read"
74
+ assert "cursor_agent_simple.py" in calls[0]["function"]["arguments"]
75
+ assert "entml:invoke" not in clean
76
+
77
+ clean_s, calls_s = _parse_stream(text, chunk=chunk)
78
+ assert len(calls_s) == 1
79
+ assert calls_s[0]["function"]["name"] == "Read"
80
+ assert "entml:invoke" not in clean_s
81
+
82
+
83
+ def test_wrong_json_object_not_parsed_as_tool() -> None:
84
+ text = f"调用工具:\n{WRONG_JSON_TOOL}"
85
+ clean, calls = _parse_batch(text)
86
+ assert calls == []
87
+ assert "Read" in clean or "name" in clean
88
+
89
+
90
+ def test_wrong_tool_block_not_parsed_as_entml_invoke() -> None:
91
+ text = f"回复\n{WRONG_TOOL_BLOCK}"
92
+ clean, calls = _parse_batch(text)
93
+ assert calls == []
94
+
95
+
96
+ def test_truncated_json_not_parsed() -> None:
97
+ clean, calls = _parse_batch(FAKE_PARTIAL_JSON)
98
+ assert calls == []
99
+
100
+
101
+ def test_thinking_then_valid_invoke(chunk: int = 5) -> None:
102
+ text = (
103
+ "<entml:thinking>\n需要先读脚本\n</entml:thinking>\n"
104
+ f"{VALID_INVOKE}"
105
+ )
106
+ clean, calls = _parse_stream(text, chunk=chunk)
107
+ assert len(calls) == 1
108
+ assert calls[0]["function"]["name"] == "Read"
109
+
110
+
111
+ def test_fault_thinking_close_then_invoke() -> None:
112
+ text = (
113
+ "<entml:thinking>\nplan\n</thinking>\n"
114
+ '<entml:invoke name="get_time">\n'
115
+ "</entml:invoke>"
116
+ )
117
+ clean, calls = _parse_stream(text, chunk=2)
118
+ assert len(calls) == 1
119
+ assert calls[0]["function"]["name"] == "get_time"
120
+ assert json.loads(calls[0]["function"]["arguments"]) == {}
121
+
122
+
123
+ def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
124
+ text = f"<entml:thinking>\nplan {VALID_INVOKE}\n"
125
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
126
+ for i in range(0, len(text), 4):
127
+ parser.feed(text[i : i + 4])
128
+ assert not parser.has_calls
129
+ assert "cursor_agent_simple.py" in parser.partial_thinking
130
+ clean, calls = parser.finalize()
131
+ assert calls == []
132
+
133
+
134
+ def test_multiline_parameter_preserved() -> None:
135
+ body = "line1\nline2\nline3"
136
+ text = (
137
+ '<entml:invoke name="Read">\n'
138
+ f'<entml:parameter name="path">{body}</entml:parameter>\n'
139
+ "</entml:invoke>"
140
+ )
141
+ _, calls = _parse_batch(text)
142
+ args = json.loads(calls[0]["function"]["arguments"])
143
+ assert args["path"] == body