echotools 2.3.50__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.50 → echotools-2.3.51}/PKG-INFO +1 -1
  2. {echotools-2.3.50 → echotools-2.3.51}/pyproject.toml +1 -1
  3. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/stream.py +113 -18
  4. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/core.py +1 -1
  5. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/parse.py +122 -9
  6. {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/PKG-INFO +1 -1
  7. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_agent_output_formats.py +4 -4
  8. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_parser_comprehensive.py +15 -15
  9. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_fncall_stream_thinking.py +9 -9
  10. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_stream_realtime_parse.py +3 -3
  11. {echotools-2.3.50 → echotools-2.3.51}/README.md +0 -0
  12. {echotools-2.3.50 → echotools-2.3.51}/docs/README.md +0 -0
  13. {echotools-2.3.50 → echotools-2.3.51}/setup.cfg +0 -0
  14. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/__init__.py +0 -0
  15. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/__init__.py +0 -0
  16. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/list_cache.py +0 -0
  17. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/memory_cache.py +0 -0
  18. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/__init__.py +0 -0
  19. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/base.py +0 -0
  20. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center.py +0 -0
  21. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center_notify.py +0 -0
  22. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center_template.py +0 -0
  23. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/loader.py +0 -0
  24. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/merge.py +0 -0
  25. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/__init__.py +0 -0
  26. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/base.py +0 -0
  27. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/classify.py +0 -0
  28. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/common.py +0 -0
  29. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/http.py +0 -0
  30. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/ids/__init__.py +0 -0
  31. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/ids/generator.py +0 -0
  32. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/__init__.py +0 -0
  33. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/io_utils.py +0 -0
  34. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/printstream.py +0 -0
  35. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/logger/__init__.py +0 -0
  36. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/logger/manager.py +0 -0
  37. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/retry/__init__.py +0 -0
  38. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/retry/retry.py +0 -0
  39. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/compat.py +0 -0
  40. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/__init__.py +0 -0
  41. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/candidate.py +0 -0
  42. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  43. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  44. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/race.py +0 -0
  45. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/selector.py +0 -0
  46. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/usage.py +0 -0
  47. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/__init__.py +0 -0
  48. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  49. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  50. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  51. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  52. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history.py +0 -0
  53. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  54. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  55. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  56. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  57. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  58. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  59. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  60. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  61. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  62. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  63. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  64. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  65. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  66. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/registry.py +0 -0
  67. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  68. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  69. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  70. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  71. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  72. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  73. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/keys.py +0 -0
  74. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/__init__.py +0 -0
  75. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/manager.py +0 -0
  76. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater.py +0 -0
  77. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  78. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/process/__init__.py +0 -0
  79. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/process/port.py +0 -0
  80. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/protocol/__init__.py +0 -0
  81. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/protocol/base.py +0 -0
  82. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/__init__.py +0 -0
  83. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/conpty.py +0 -0
  84. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/__init__.py +0 -0
  85. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/core.py +0 -0
  86. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/proc.py +0 -0
  87. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/unix.py +0 -0
  88. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/win.py +0 -0
  89. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/sanitize.py +0 -0
  90. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/__init__.py +0 -0
  91. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/base.py +0 -0
  92. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/client.py +0 -0
  93. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/ssh.py +0 -0
  94. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/tmux.py +0 -0
  95. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/__init__.py +0 -0
  96. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/charmap.py +0 -0
  97. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/spinner.py +0 -0
  98. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/ui.py +0 -0
  99. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  100. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  101. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  102. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  103. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  104. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  105. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_console.py +0 -0
  106. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_io.py +0 -0
  107. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_log.py +0 -0
  108. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  109. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_text.py +0 -0
  110. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_types.py +0 -0
  111. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  112. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  113. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  114. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  115. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  116. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  117. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  118. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  119. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  120. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  121. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  122. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/__init__.py +0 -0
  123. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/bus.py +0 -0
  124. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/event.py +0 -0
  125. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/__init__.py +0 -0
  126. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/spinner.py +0 -0
  127. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/spinner_color.py +0 -0
  128. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/__init__.py +0 -0
  129. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/context.py +0 -0
  130. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/span.py +0 -0
  131. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/tracer.py +0 -0
  132. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/translate.py +0 -0
  133. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/watcher/__init__.py +0 -0
  134. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/watcher/file_watcher.py +0 -0
  135. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/__init__.py +0 -0
  136. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/application.py +0 -0
  137. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/broker.py +0 -0
  138. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/app.js +0 -0
  139. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  140. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  141. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  142. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  143. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  144. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  145. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  146. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  147. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  148. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  149. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.css +0 -0
  150. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.js +0 -0
  151. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/style.css +0 -0
  152. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/voicegif.js +0 -0
  153. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/middleware.py +0 -0
  154. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/stats.py +0 -0
  155. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/utils.py +0 -0
  156. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/files/__init__.py +0 -0
  157. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/files/file_util.py +0 -0
  158. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/network/__init__.py +0 -0
  159. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/network/http_utils.py +0 -0
  160. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/__init__.py +0 -0
  161. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/base.py +0 -0
  162. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/discovery.py +0 -0
  163. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/registry.py +0 -0
  164. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/registry_ops.py +0 -0
  165. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/proxy/__init__.py +0 -0
  166. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/proxy/manager.py +0 -0
  167. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/runtime/__init__.py +0 -0
  168. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/runtime/collector.py +0 -0
  169. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/scheduler/__init__.py +0 -0
  170. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/scheduler/scheduler.py +0 -0
  171. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/sdk/__init__.py +0 -0
  172. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/sdk/facade.py +0 -0
  173. {echotools-2.3.50 → echotools-2.3.51}/src/echotools/version.py +0 -0
  174. {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/SOURCES.txt +0 -0
  175. {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/dependency_links.txt +0 -0
  176. {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/requires.txt +0 -0
  177. {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/top_level.txt +0 -0
  178. {echotools-2.3.50 → echotools-2.3.51}/src/tests/bench_import.py +0 -0
  179. {echotools-2.3.50 → echotools-2.3.51}/src/tests/conftest.py +0 -0
  180. {echotools-2.3.50 → echotools-2.3.51}/src/tests/fixtures/__init__.py +0 -0
  181. {echotools-2.3.50 → echotools-2.3.51}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  182. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_adversarial_stream_splits.py +0 -0
  183. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_cache_retry.py +0 -0
  184. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_completion.py +0 -0
  185. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_config.py +0 -0
  186. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_config_extended.py +0 -0
  187. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_console.py +0 -0
  188. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_coverage_core.py +0 -0
  189. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch.py +0 -0
  190. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch_extended.py +0 -0
  191. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch_race.py +0 -0
  192. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_bare_invoke_format.py +0 -0
  193. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_protocol.py +0 -0
  194. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_stream_json.py +0 -0
  195. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  196. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_thinking_history.py +0 -0
  197. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_thinking_parse.py +0 -0
  198. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tool_parse_leak.py +0 -0
  199. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tool_pipeline.py +0 -0
  200. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tools.py +0 -0
  201. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_errors.py +0 -0
  202. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_errors_common.py +0 -0
  203. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_events.py +0 -0
  204. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_events_event.py +0 -0
  205. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_facade.py +0 -0
  206. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_fncall.py +0 -0
  207. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_ids.py +0 -0
  208. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_imports.py +0 -0
  209. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_io.py +0 -0
  210. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_keys.py +0 -0
  211. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_lifecycle.py +0 -0
  212. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_logger.py +0 -0
  213. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_merge.py +0 -0
  214. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_plugin.py +0 -0
  215. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_printstream.py +0 -0
  216. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_protocols.py +0 -0
  217. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_proxy.py +0 -0
  218. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_proxy_selector.py +0 -0
  219. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_runtime.py +0 -0
  220. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_scheduler.py +0 -0
  221. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  222. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_tracing.py +0 -0
  223. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_usage.py +0 -0
  224. {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_web.py +0 -0
  225. {echotools-2.3.50 → 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.50
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.50"
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,6 +165,9 @@ 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
+
151
171
  def _drain_thinking_holdback_to_fncall(self) -> None:
152
172
  """invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
153
173
  if self._thinking_filter is None:
@@ -162,14 +182,47 @@ class FncallStreamParser:
162
182
  self._drain_thinking_holdback_to_fncall()
163
183
  self._detected = True
164
184
  self._state = self.IN_FUNCTION_CALLS
165
- if self._is_call_closed():
166
- self._state = self.DONE
185
+ self._json_stream_encoder = None
186
+ if self._thinking_filter is not None:
187
+ self._thinking_filter.clear_invoke_handoff_state()
167
188
 
168
189
  def _feed_content_waiting(self, text: str) -> None:
169
190
  """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
170
191
  if not text:
171
192
  return
172
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
173
226
  trigger_tags = self._protocol.get_trigger_tags()
174
227
  found, pos = self._protocol.detect_start(text)
175
228
  if not found:
@@ -204,10 +257,24 @@ class FncallStreamParser:
204
257
  self._drain_thinking_holdback_to_fncall()
205
258
 
206
259
  def _feed_waiting(self, chunk: str) -> None:
207
- """在 WAITING_FOR_TAG 状态下处理新块。"""
260
+ """在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
208
261
  combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
209
262
  self._waiting_tail = ""
210
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
+
211
278
  # 已进入 thinking 块时,块内不做 invoke 检测。
212
279
  if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
213
280
  self._feed_waiting_thinking_plain(combined)
@@ -279,29 +346,20 @@ class FncallStreamParser:
279
346
 
280
347
  self._raw_buf += chunk
281
348
 
282
- if self._state == self.WAITING_FOR_TAG:
349
+ if self._state != self.DONE:
283
350
  self._feed_waiting(chunk)
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()
289
- self._fncall_buf += self._normalize_stream_chunk(chunk)
290
- if self._is_call_closed():
291
- self._state = self.DONE
292
351
 
293
352
  self._pending_stream_delta = self._poll_streaming_tool_input_delta()
294
353
  ready = self.get_ready_tool_calls()
295
- if ready:
296
- if self._json_stream_encoder is not None:
297
- self._json_stream_encoder.reset()
354
+ if ready and self._json_stream_encoder is not None:
355
+ self._json_stream_encoder.reset()
298
356
  return ready
299
357
 
300
358
  def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
301
359
  """invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
302
360
  if not self._detected or getattr(self._protocol, "id", None) != "entml":
303
361
  return None
304
- if self._state not in (self.IN_FUNCTION_CALLS, self.DONE):
362
+ if self._state not in (self.IN_FUNCTION_CALLS,):
305
363
  return None
306
364
  from echotools.exec.fncall.protocols.entml_stream_json import (
307
365
  EntmlInvokeJsonStreamEncoder,
@@ -326,6 +384,43 @@ class FncallStreamParser:
326
384
  self._pending_stream_delta = None
327
385
  return delta
328
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
+
329
424
  def _assembly_for_tool_parse(self) -> str:
330
425
  """可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
331
426
  return "".join(self._text_parts) + self._waiting_tail + self._fncall_buf
@@ -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, ""
@@ -166,6 +226,13 @@ class EntmlThinkingStreamFilter:
166
226
  self._fault_watch = False
167
227
  self._fault_buffer = ""
168
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
+
169
236
  def in_open_thinking(self) -> bool:
170
237
  """思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
171
238
  if self._in_block or self._fault_watch:
@@ -219,14 +286,56 @@ class EntmlThinkingStreamFilter:
219
286
  return True
220
287
  return False
221
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
+
222
329
  def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
223
330
  """处理块内 pending;返回 False 表示应退出 feed 主循环。"""
224
331
  close_at = self._pending.find(_THINKING_CLOSE)
225
332
  if close_at >= 0:
226
333
  piece = self._pending[:close_at]
227
- emitted = self._emit_thinking_piece(piece)
228
- if emitted:
229
- 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))
230
339
  self._in_block = False
231
340
  self._fault_watch = False
232
341
  self._fault_buffer = ""
@@ -234,6 +343,9 @@ class EntmlThinkingStreamFilter:
234
343
  self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
235
344
  return True
236
345
 
346
+ if self._try_emit_actionable_invoke_from_in_block(out):
347
+ return True
348
+
237
349
  fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
238
350
  if fault_at >= 0:
239
351
  before = self._pending[:fault_at]
@@ -337,10 +449,11 @@ class EntmlThinkingStreamFilter:
337
449
  self._in_block = False
338
450
  self._thinking_started = False
339
451
  if self._in_block:
340
- emitted = self._emit_thinking_piece(self._pending)
341
- if emitted:
342
- out.append(("thinking", emitted))
343
- 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 = ""
344
457
  self._in_block = False
345
458
  self._thinking_started = False
346
459
  if self._pending:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.50
3
+ Version: 2.3.51
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -120,15 +120,15 @@ def test_fault_thinking_close_then_invoke() -> None:
120
120
  assert json.loads(calls[0]["function"]["arguments"]) == {}
121
121
 
122
122
 
123
- def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
123
+ def test_invoke_inside_unclosed_thinking_parsed_when_complete() -> None:
124
124
  text = f"<entml:thinking>\nplan {VALID_INVOKE}\n"
125
125
  parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
126
126
  for i in range(0, len(text), 4):
127
127
  parser.feed(text[i : i + 4])
128
- assert not parser.has_calls
129
- assert "cursor_agent_simple.py" in parser.partial_thinking
128
+ assert parser.has_calls
130
129
  clean, calls = parser.finalize()
131
- assert calls == []
130
+ assert len(calls) == 1
131
+ assert calls[0]["function"]["name"] == "Read"
132
132
 
133
133
 
134
134
  def test_multiline_parameter_preserved() -> None:
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  from dataclasses import dataclass, field
12
- from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
12
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
13
13
 
14
14
  import pytest
15
15
 
@@ -278,16 +278,18 @@ def _scenarios() -> List[ParserScenario]:
278
278
  id="invoke_inside_unclosed_thinking",
279
279
  text=f"<entml:thinking>\nplan\n{READ_INVOKE}\n",
280
280
  tools=[READ_TOOL],
281
- expect_call_count=0,
282
- expect_thinking_contains=("plan", "cursor_agent_simple.py"),
281
+ expect_names=("Read",),
282
+ expect_args=({"path": READ_PATH},),
283
+ expect_thinking_contains=("plan",),
283
284
  stream_only=True,
284
285
  ),
285
286
  ParserScenario(
286
287
  id="invoke_and_example_inside_closed_thinking",
287
288
  text=example_in_thinking,
288
289
  tools=[READ_TOOL],
289
- expect_call_count=0,
290
- expect_thinking_contains=("格式示例", "cursor_agent_simple.py"),
290
+ expect_names=("Read",),
291
+ expect_args=({"path": READ_PATH},),
292
+ expect_thinking_contains=("格式示例", "$FUNCTION_NAME"),
291
293
  stream_only=True,
292
294
  ),
293
295
  ParserScenario(
@@ -559,29 +561,27 @@ def test_invoke_index_inside_unclosed_thinking() -> None:
559
561
  assert not invoke_index_inside_unclosed_thinking(closed, pos3)
560
562
 
561
563
 
562
- def test_large_chunk_invoke_inside_unclosed_thinking_zero_calls() -> None:
563
- """回归:首包很大且含 thinking 内 invoke 时不得进入 IN_FUNCTION_CALLS。"""
564
+ def test_large_chunk_invoke_inside_unclosed_thinking_parsed() -> None:
565
+ """回归:首包很大且含 thinking 内完整 invoke 时应解析工具。"""
564
566
  text = f"<entml:thinking>\nplan\n{READ_INVOKE}\n"
565
567
  parser = FncallStreamParser(protocol=get_protocol("entml"), tools=[READ_TOOL])
566
568
  parser.feed(text[:64])
567
- assert parser.state == FncallStreamParser.WAITING_FOR_TAG
568
569
  assert parser._thinking_filter is not None
569
- assert parser._thinking_filter.in_open_thinking()
570
570
  parser.feed(text[64:])
571
571
  _, calls = parser.finalize()
572
- assert calls == []
573
- assert "cursor_agent_simple.py" in parser.partial_thinking
572
+ assert len(calls) == 1
573
+ assert calls[0]["function"]["name"] == "Read"
574
574
 
575
575
 
576
- def test_batch_thinking_invoke_known_gap() -> None:
577
- """文档化:批量 parse 会解析 thinking 块内的 invoke(流式不会)。"""
576
+ def test_batch_thinking_invoke_stream_parses_complete_inside() -> None:
577
+ """流式与批量均解析 thinking 内已闭合的真实 invoke。"""
578
578
  proto = get_protocol("entml")
579
579
  inside = f"<entml:thinking>\nplan\n{READ_INVOKE}\n</entml:thinking>\n"
580
580
  _, batch_calls = proto.parse(inside, [READ_TOOL])
581
581
  assert len(batch_calls) >= 1
582
582
  stream_clean, stream_calls, _ = _stream_parse(inside, [READ_TOOL], 8)
583
- assert len(stream_calls) == 0
584
- assert "entml:invoke" not in stream_clean
583
+ assert len(stream_calls) == 1
584
+ assert stream_calls[0]["function"]["name"] == "Read"
585
585
 
586
586
 
587
587
  def test_char_by_char_all_scenarios_with_tools() -> None:
@@ -55,16 +55,16 @@ def test_split_entml_thinking_ignores_unclosed_block() -> None:
55
55
  assert INVOKE in content
56
56
 
57
57
 
58
- def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
59
- """未闭合 thinking 内的 entml 标签仅作思考文本,不解析为 tool call。"""
58
+ def test_invoke_inside_unclosed_thinking_is_parsed_when_complete() -> None:
59
+ """thinking 内完整的真实 invoke 应切出并解析(占位符示例除外)。"""
60
60
  parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
61
61
  stream = f"<entml:thinking>\nplan {INVOKE}\n"
62
62
  for i in range(0, len(stream), 7):
63
63
  parser.feed(stream[i : i + 7])
64
- assert not parser.has_calls
65
- assert INVOKE in parser.partial_thinking or "entml:invoke" in parser.partial_thinking
64
+ assert parser.has_calls
66
65
  clean, calls = parser.finalize()
67
- assert len(calls) == 0
66
+ assert len(calls) == 1
67
+ assert calls[0]["function"]["name"] == "get_weather"
68
68
 
69
69
 
70
70
  def test_invoke_after_thinking_close_is_parsed() -> None:
@@ -97,18 +97,18 @@ def test_no_thinking_invoke_still_works() -> None:
97
97
  assert "Sure" in clean
98
98
 
99
99
 
100
- def test_stream_thinking_invoke_inside_stays_thinking_until_close() -> None:
100
+ def test_stream_thinking_invoke_inside_parsed_when_complete() -> None:
101
101
  parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
102
102
  parser.feed("<entml:thinking>\n")
103
103
  parser.feed("line one\n")
104
104
  assert "line one" in parser.partial_thinking
105
105
  assert not parser.has_calls
106
106
  parser.feed(f"mention {INVOKE}\n")
107
- assert not parser.has_calls
108
- assert "entml:invoke" in parser.partial_thinking
107
+ assert parser.has_calls
109
108
  parser.feed("</entml:thinking>\nanswer")
110
109
  clean, calls = parser.finalize()
111
- assert len(calls) == 0
110
+ assert len(calls) == 1
111
+ assert calls[0]["function"]["name"] == "get_weather"
112
112
  assert "answer" in clean
113
113
 
114
114
 
@@ -112,7 +112,7 @@ def test_char_by_char_ready_matches_final_for_all_tool_cases() -> None:
112
112
  assert _args(incremental) == _args(final) == case.expect_args, case.id
113
113
 
114
114
 
115
- def test_unclosed_thinking_invoke_not_parsed_until_close() -> None:
115
+ def test_unclosed_thinking_invoke_parsed_when_complete_inside() -> None:
116
116
  proto = get_protocol("entml")
117
117
  tools = TOOLS
118
118
  invoke = (
@@ -124,8 +124,8 @@ def test_unclosed_thinking_invoke_not_parsed_until_close() -> None:
124
124
  ready = []
125
125
  for ch in f"<entml:thinking>\nplan {invoke}\n":
126
126
  ready.extend(parser.feed(ch))
127
- assert ready == []
128
- for ch in f"</entml:thinking>\n可见\n{invoke}":
127
+ assert _names(ready) == ["get_weather"]
128
+ for ch in "</entml:thinking>\n可见":
129
129
  ready.extend(parser.feed(ch))
130
130
  ready.extend(parser.get_ready_tool_calls())
131
131
  _, final = parser.finalize()
File without changes
File without changes
File without changes