echotools 2.3.39__tar.gz → 2.3.40__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 (190) hide show
  1. {echotools-2.3.39 → echotools-2.3.40}/PKG-INFO +1 -1
  2. {echotools-2.3.39 → echotools-2.3.40}/pyproject.toml +1 -1
  3. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/stream.py +48 -2
  4. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_parse.py +39 -0
  5. {echotools-2.3.39 → echotools-2.3.40}/src/echotools.egg-info/PKG-INFO +1 -1
  6. {echotools-2.3.39 → echotools-2.3.40}/src/echotools.egg-info/SOURCES.txt +1 -0
  7. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_entml_thinking_parse.py +6 -0
  8. echotools-2.3.40/src/tests/test_fncall_stream_thinking.py +110 -0
  9. {echotools-2.3.39 → echotools-2.3.40}/README.md +0 -0
  10. {echotools-2.3.39 → echotools-2.3.40}/docs/README.md +0 -0
  11. {echotools-2.3.39 → echotools-2.3.40}/setup.cfg +0 -0
  12. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/__init__.py +0 -0
  13. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/cache/__init__.py +0 -0
  14. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/cache/list_cache.py +0 -0
  15. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/cache/memory_cache.py +0 -0
  16. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/__init__.py +0 -0
  17. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/base.py +0 -0
  18. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/center.py +0 -0
  19. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/center_notify.py +0 -0
  20. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/center_template.py +0 -0
  21. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/loader.py +0 -0
  22. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/config/merge.py +0 -0
  23. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/errors/__init__.py +0 -0
  24. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/errors/base.py +0 -0
  25. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/errors/classify.py +0 -0
  26. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/errors/common.py +0 -0
  27. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/errors/http.py +0 -0
  28. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/ids/__init__.py +0 -0
  29. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/ids/generator.py +0 -0
  30. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/io/__init__.py +0 -0
  31. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/io/io_utils.py +0 -0
  32. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/io/printstream.py +0 -0
  33. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/logger/__init__.py +0 -0
  34. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/logger/manager.py +0 -0
  35. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/retry/__init__.py +0 -0
  36. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/base/retry/retry.py +0 -0
  37. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/compat.py +0 -0
  38. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/__init__.py +0 -0
  39. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/candidate.py +0 -0
  40. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  41. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  42. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/race.py +0 -0
  43. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/selector.py +0 -0
  44. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/dispatch/usage.py +0 -0
  45. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/__init__.py +0 -0
  46. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  47. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  48. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  49. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  50. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history.py +0 -0
  51. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  52. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  53. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  54. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  55. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  56. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  57. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  58. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  59. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_sanitize.py +0 -0
  60. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking.py +0 -0
  61. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_history.py +0 -0
  62. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  63. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  64. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/registry.py +0 -0
  65. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  66. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  67. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  68. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  69. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  70. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  71. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/keys.py +0 -0
  72. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/lifecycle/__init__.py +0 -0
  73. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/lifecycle/manager.py +0 -0
  74. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater.py +0 -0
  75. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  76. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/process/__init__.py +0 -0
  77. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/process/port.py +0 -0
  78. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/protocol/__init__.py +0 -0
  79. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/protocol/base.py +0 -0
  80. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/__init__.py +0 -0
  81. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/conpty.py +0 -0
  82. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/local/__init__.py +0 -0
  83. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/local/core.py +0 -0
  84. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/local/proc.py +0 -0
  85. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/local/unix.py +0 -0
  86. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/local/win.py +0 -0
  87. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/sanitize.py +0 -0
  88. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/session/__init__.py +0 -0
  89. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/session/base.py +0 -0
  90. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/session/client.py +0 -0
  91. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/ssh.py +0 -0
  92. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/exec/terminal/tmux.py +0 -0
  93. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/console/__init__.py +0 -0
  94. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/console/charmap.py +0 -0
  95. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/console/spinner.py +0 -0
  96. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/console/ui.py +0 -0
  97. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/events/__init__.py +0 -0
  98. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/events/bus.py +0 -0
  99. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/events/event.py +0 -0
  100. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/spinner/__init__.py +0 -0
  101. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/spinner/spinner.py +0 -0
  102. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/spinner/spinner_color.py +0 -0
  103. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/tracing/__init__.py +0 -0
  104. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/tracing/context.py +0 -0
  105. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/tracing/span.py +0 -0
  106. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/tracing/tracer.py +0 -0
  107. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/translate.py +0 -0
  108. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/watcher/__init__.py +0 -0
  109. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/watcher/file_watcher.py +0 -0
  110. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/__init__.py +0 -0
  111. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/application.py +0 -0
  112. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/broker.py +0 -0
  113. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/app.js +0 -0
  114. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  115. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  116. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  117. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  118. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  119. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  120. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  121. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  122. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  123. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  124. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
  125. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.css +0 -0
  126. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.js +0 -0
  127. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/style.css +0 -0
  128. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/input_box/voicegif.js +0 -0
  129. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/middleware.py +0 -0
  130. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/stats.py +0 -0
  131. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/media/web/utils.py +0 -0
  132. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/files/__init__.py +0 -0
  133. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/files/file_util.py +0 -0
  134. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/network/__init__.py +0 -0
  135. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/network/http_utils.py +0 -0
  136. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/plugin/__init__.py +0 -0
  137. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/plugin/base.py +0 -0
  138. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/plugin/discovery.py +0 -0
  139. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/plugin/registry.py +0 -0
  140. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/plugin/registry_ops.py +0 -0
  141. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/proxy/__init__.py +0 -0
  142. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/proxy/manager.py +0 -0
  143. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/runtime/__init__.py +0 -0
  144. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/runtime/collector.py +0 -0
  145. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/scheduler/__init__.py +0 -0
  146. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/scheduler/scheduler.py +0 -0
  147. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/sdk/__init__.py +0 -0
  148. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/plat/sdk/facade.py +0 -0
  149. {echotools-2.3.39 → echotools-2.3.40}/src/echotools/version.py +0 -0
  150. {echotools-2.3.39 → echotools-2.3.40}/src/echotools.egg-info/dependency_links.txt +0 -0
  151. {echotools-2.3.39 → echotools-2.3.40}/src/echotools.egg-info/requires.txt +0 -0
  152. {echotools-2.3.39 → echotools-2.3.40}/src/echotools.egg-info/top_level.txt +0 -0
  153. {echotools-2.3.39 → echotools-2.3.40}/src/tests/bench_import.py +0 -0
  154. {echotools-2.3.39 → echotools-2.3.40}/src/tests/conftest.py +0 -0
  155. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_cache_retry.py +0 -0
  156. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_completion.py +0 -0
  157. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_config.py +0 -0
  158. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_config_extended.py +0 -0
  159. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_console.py +0 -0
  160. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_coverage_core.py +0 -0
  161. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_dispatch.py +0 -0
  162. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_dispatch_extended.py +0 -0
  163. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_dispatch_race.py +0 -0
  164. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_entml_protocol.py +0 -0
  165. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_entml_thinking_history.py +0 -0
  166. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_entml_tools.py +0 -0
  167. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_errors.py +0 -0
  168. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_errors_common.py +0 -0
  169. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_events.py +0 -0
  170. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_events_event.py +0 -0
  171. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_facade.py +0 -0
  172. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_fncall.py +0 -0
  173. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_ids.py +0 -0
  174. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_imports.py +0 -0
  175. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_io.py +0 -0
  176. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_keys.py +0 -0
  177. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_lifecycle.py +0 -0
  178. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_logger.py +0 -0
  179. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_merge.py +0 -0
  180. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_plugin.py +0 -0
  181. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_printstream.py +0 -0
  182. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_protocols.py +0 -0
  183. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_proxy.py +0 -0
  184. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_proxy_selector.py +0 -0
  185. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_runtime.py +0 -0
  186. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_scheduler.py +0 -0
  187. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_tracing.py +0 -0
  188. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_usage.py +0 -0
  189. {echotools-2.3.39 → echotools-2.3.40}/src/tests/test_web.py +0 -0
  190. {echotools-2.3.39 → echotools-2.3.40}/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.39
3
+ Version: 2.3.40
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.39"
7
+ version = "2.3.40"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -136,12 +136,59 @@ class FncallStreamParser:
136
136
 
137
137
  return buffer, ""
138
138
 
139
+ def _append_content_text(self, text: str) -> None:
140
+ if text:
141
+ self._text_parts.append(text)
142
+
143
+ def _feed_content_waiting(self, text: str) -> None:
144
+ """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
145
+ if not text:
146
+ return
147
+ trigger_tags = self._protocol.get_trigger_tags()
148
+ found, pos = self._protocol.detect_start(text)
149
+ if not found:
150
+ safe, remain = self._split_safe_text(text, trigger_tags)
151
+ if safe:
152
+ self._append_content_text(safe)
153
+ self._waiting_tail = remain
154
+ return
155
+
156
+ if pos > 0:
157
+ self._append_content_text(text[:pos])
158
+
159
+ self._fncall_buf = text[pos:]
160
+ self._detected = True
161
+ self._state = self.IN_FUNCTION_CALLS
162
+ if self._is_call_closed():
163
+ self._state = self.DONE
164
+
165
+ def _feed_waiting_thinking_plain(self, combined: str) -> None:
166
+ """未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
167
+ assert self._thinking_filter is not None
168
+ for kind, part in self._thinking_filter.feed(combined):
169
+ if kind == "thinking":
170
+ self._thinking_parts.append(part)
171
+ elif part:
172
+ self._feed_content_waiting(part)
173
+
139
174
  def _feed_waiting(self, chunk: str) -> None:
140
175
  """在 WAITING_FOR_TAG 状态下处理新块。"""
141
176
  combined = self._waiting_tail + chunk
142
- found, pos = self._protocol.detect_start(combined)
177
+ self._waiting_tail = ""
178
+
179
+ if self._thinking_filter is not None:
180
+ from echotools.exec.fncall.protocols.entml_thinking_parse import (
181
+ has_unclosed_entml_thinking,
182
+ )
183
+ if (
184
+ self._thinking_filter.in_open_thinking()
185
+ or has_unclosed_entml_thinking(combined)
186
+ ):
187
+ self._feed_waiting_thinking_plain(combined)
188
+ return
143
189
 
144
190
  trigger_tags = self._protocol.get_trigger_tags()
191
+ found, pos = self._protocol.detect_start(combined)
145
192
 
146
193
  if not found:
147
194
  safe, remain = self._split_safe_text(combined, trigger_tags)
@@ -154,7 +201,6 @@ class FncallStreamParser:
154
201
  self._emit_text(combined[:pos])
155
202
 
156
203
  self._fncall_buf = combined[pos:]
157
- self._waiting_tail = ""
158
204
  self._detected = True
159
205
  self._state = self.IN_FUNCTION_CALLS
160
206
 
@@ -12,6 +12,31 @@ _THINKING_OPEN_PREFIX = "<entml:thinking"
12
12
  _THINKING_CLOSE = "</entml:thinking>"
13
13
 
14
14
 
15
+ def has_unclosed_entml_thinking(text: str) -> bool:
16
+ """buffer 中是否存在尚未闭合的 <entml:thinking> 块(含开标签未收齐)。"""
17
+ if not text:
18
+ return False
19
+ i = 0
20
+ while i < len(text):
21
+ open_at = text.find(_THINKING_OPEN_PREFIX, i)
22
+ if open_at < 0:
23
+ break
24
+ gt = text.find(">", open_at)
25
+ if gt < 0:
26
+ return True
27
+ close_at = text.find(_THINKING_CLOSE, gt + 1)
28
+ if close_at < 0:
29
+ return True
30
+ i = close_at + len(_THINKING_CLOSE)
31
+ max_keep = len(_THINKING_OPEN_PREFIX) - 1
32
+ check_len = min(len(text), max_keep)
33
+ for length in range(check_len, 0, -1):
34
+ suffix = text[-length:]
35
+ if _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX:
36
+ return True
37
+ return False
38
+
39
+
15
40
  def split_entml_thinking(text: str) -> Tuple[str, str]:
16
41
  """从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
17
42
  if not text:
@@ -53,6 +78,20 @@ class EntmlThinkingStreamFilter:
53
78
  # 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
54
79
  self._thinking_started = False
55
80
 
81
+ def in_open_thinking(self) -> bool:
82
+ """思考块已开始且尚未收到 </entml:thinking>(含开/闭标签分片 hold)。"""
83
+ if self._in_block:
84
+ return True
85
+ open_at = self._pending.find(_THINKING_OPEN_PREFIX)
86
+ if open_at >= 0:
87
+ gt = self._pending.find(">", open_at)
88
+ if gt < 0:
89
+ return True
90
+ _, hold = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
91
+ if hold:
92
+ return True
93
+ return False
94
+
56
95
  def feed(self, chunk: str) -> List[Tuple[str, str]]:
57
96
  """返回 [(kind, text), ...],kind 为 content 或 thinking。"""
58
97
  if not chunk:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.39
3
+ Version: 2.3.40
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -167,6 +167,7 @@ src/tests/test_events.py
167
167
  src/tests/test_events_event.py
168
168
  src/tests/test_facade.py
169
169
  src/tests/test_fncall.py
170
+ src/tests/test_fncall_stream_thinking.py
170
171
  src/tests/test_ids.py
171
172
  src/tests/test_imports.py
172
173
  src/tests/test_io.py
@@ -1,5 +1,6 @@
1
1
  from echotools.exec.fncall.protocols.entml_thinking_parse import (
2
2
  EntmlThinkingStreamFilter,
3
+ has_unclosed_entml_thinking,
3
4
  split_entml_thinking,
4
5
  )
5
6
 
@@ -16,6 +17,11 @@ def test_split_entml_thinking() -> None:
16
17
  assert "<entml:thinking>" not in content
17
18
 
18
19
 
20
+ def test_has_unclosed_entml_thinking_streaming_prefix() -> None:
21
+ assert has_unclosed_entml_thinking("<entml:think")
22
+ assert not has_unclosed_entml_thinking("<entml:thinking>done</entml:thinking>")
23
+
24
+
19
25
  def test_entml_thinking_stream_filter() -> None:
20
26
  filt = EntmlThinkingStreamFilter()
21
27
  parts = []
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from echotools.exec.fncall import get_protocol
4
+ from echotools.exec.fncall.parsers.stream import FncallStreamParser
5
+ from echotools.exec.fncall.protocols.entml_thinking_parse import (
6
+ EntmlThinkingStreamFilter,
7
+ has_unclosed_entml_thinking,
8
+ split_entml_thinking,
9
+ )
10
+
11
+ TOOLS = [
12
+ {
13
+ "type": "function",
14
+ "function": {
15
+ "name": "get_weather",
16
+ "parameters": {
17
+ "type": "object",
18
+ "properties": {"city": {"type": "string"}},
19
+ "required": ["city"],
20
+ },
21
+ },
22
+ }
23
+ ]
24
+
25
+ INVOKE = (
26
+ '<entml:invoke name="get_weather">'
27
+ '<entml:parameter name="city">Hangzhou</entml:parameter>'
28
+ "</entml:invoke>"
29
+ )
30
+
31
+
32
+ def test_has_unclosed_entml_thinking() -> None:
33
+ assert not has_unclosed_entml_thinking("")
34
+ assert not has_unclosed_entml_thinking("hello")
35
+ assert has_unclosed_entml_thinking("<entml:thinking>\nstep")
36
+ assert has_unclosed_entml_thinking("<entml:thinking>\nstep</entml:think")
37
+ assert not has_unclosed_entml_thinking(
38
+ "<entml:thinking>\nstep\n</entml:thinking>\nanswer"
39
+ )
40
+
41
+
42
+ def test_in_open_thinking_hold_open_tag() -> None:
43
+ filt = EntmlThinkingStreamFilter()
44
+ assert not filt.in_open_thinking()
45
+ filt.feed("prefix <entml:think")
46
+ assert filt.in_open_thinking()
47
+
48
+
49
+ def test_split_entml_thinking_ignores_unclosed_block() -> None:
50
+ text = "<entml:thinking>still going\n" + INVOKE
51
+ content, thinking = split_entml_thinking(text)
52
+ assert thinking == ""
53
+ assert INVOKE in content
54
+
55
+
56
+ def test_invoke_inside_unclosed_thinking_not_parsed_as_tool() -> None:
57
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
58
+ stream = f"<entml:thinking>\nplan {INVOKE}\n"
59
+ for i in range(0, len(stream), 7):
60
+ parser.feed(stream[i : i + 7])
61
+ assert not parser.has_calls
62
+ assert INVOKE in parser.partial_thinking
63
+ clean, calls = parser.finalize()
64
+ assert not calls
65
+ assert INVOKE not in clean
66
+
67
+
68
+ def test_invoke_after_thinking_close_is_parsed() -> None:
69
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
70
+ text = f"<entml:thinking>ok</entml:thinking>\n\n{INVOKE}"
71
+ parser.feed(text)
72
+ assert parser.has_calls
73
+ clean, calls = parser.finalize()
74
+ assert len(calls) == 1
75
+ assert calls[0]["function"]["name"] == "get_weather"
76
+ assert "Hangzhou" in calls[0]["function"]["arguments"]
77
+
78
+
79
+ def test_thinking_close_and_invoke_in_one_chunk() -> None:
80
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
81
+ chunks = ["<entml:thinking>reason</entml:thinking>", INVOKE]
82
+ for chunk in chunks:
83
+ parser.feed(chunk)
84
+ clean, calls = parser.finalize()
85
+ assert parser.partial_thinking.strip() == "reason"
86
+ assert len(calls) == 1
87
+ assert "reason" not in clean or clean.strip() == ""
88
+
89
+
90
+ def test_no_thinking_invoke_still_works() -> None:
91
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
92
+ parser.feed(f"Sure.\n{INVOKE}")
93
+ clean, calls = parser.finalize()
94
+ assert len(calls) == 1
95
+ assert "Sure" in clean
96
+
97
+
98
+ def test_stream_thinking_incremental_before_close() -> None:
99
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
100
+ parser.feed("<entml:thinking>\n")
101
+ parser.feed("line one\n")
102
+ assert "line one" in parser.partial_thinking
103
+ assert not parser.has_calls
104
+ parser.feed(f"mention {INVOKE}\n")
105
+ assert INVOKE in parser.partial_thinking
106
+ assert not parser.has_calls
107
+ parser.feed("</entml:thinking>\nanswer")
108
+ clean, calls = parser.finalize()
109
+ assert not calls
110
+ assert "answer" in clean
File without changes
File without changes
File without changes