echotools 2.3.38__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.38 → echotools-2.3.40}/PKG-INFO +1 -1
  2. {echotools-2.3.38 → echotools-2.3.40}/pyproject.toml +5 -1
  3. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/stream.py +55 -4
  4. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/inject.py +4 -1
  5. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/prompt_helpers.py +6 -6
  6. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml.py +0 -1
  7. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -1
  8. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_parse.py +39 -0
  9. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/spinner.py +4 -4
  10. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/ui.py +28 -29
  11. {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/PKG-INFO +1 -1
  12. {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/SOURCES.txt +1 -0
  13. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_console.py +0 -2
  14. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_thinking_parse.py +6 -0
  15. echotools-2.3.40/src/tests/test_fncall_stream_thinking.py +110 -0
  16. {echotools-2.3.38 → echotools-2.3.40}/README.md +0 -0
  17. {echotools-2.3.38 → echotools-2.3.40}/docs/README.md +0 -0
  18. {echotools-2.3.38 → echotools-2.3.40}/setup.cfg +0 -0
  19. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/__init__.py +0 -0
  20. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/__init__.py +0 -0
  21. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/list_cache.py +0 -0
  22. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/cache/memory_cache.py +0 -0
  23. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/__init__.py +0 -0
  24. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/base.py +0 -0
  25. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center.py +0 -0
  26. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center_notify.py +0 -0
  27. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/center_template.py +0 -0
  28. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/loader.py +0 -0
  29. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/config/merge.py +0 -0
  30. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/__init__.py +0 -0
  31. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/base.py +0 -0
  32. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/classify.py +0 -0
  33. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/common.py +0 -0
  34. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/errors/http.py +0 -0
  35. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/ids/__init__.py +0 -0
  36. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/ids/generator.py +0 -0
  37. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/__init__.py +0 -0
  38. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/io_utils.py +0 -0
  39. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/io/printstream.py +0 -0
  40. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/logger/__init__.py +0 -0
  41. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/logger/manager.py +0 -0
  42. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/retry/__init__.py +0 -0
  43. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/base/retry/retry.py +0 -0
  44. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/compat.py +0 -0
  45. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/__init__.py +0 -0
  46. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/candidate.py +0 -0
  47. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  48. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  49. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/race.py +0 -0
  50. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/selector.py +0 -0
  51. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/dispatch/usage.py +0 -0
  52. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/__init__.py +0 -0
  53. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  54. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  55. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  56. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  57. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history.py +0 -0
  58. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  59. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  60. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  61. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  62. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_sanitize.py +0 -0
  63. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking.py +0 -0
  64. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_thinking_history.py +0 -0
  65. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  66. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  67. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/registry.py +0 -0
  68. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  69. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  70. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  71. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  72. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  73. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  74. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/keys.py +0 -0
  75. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/__init__.py +0 -0
  76. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/manager.py +0 -0
  77. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater.py +0 -0
  78. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  79. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/process/__init__.py +0 -0
  80. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/process/port.py +0 -0
  81. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/protocol/__init__.py +0 -0
  82. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/protocol/base.py +0 -0
  83. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/__init__.py +0 -0
  84. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/conpty.py +0 -0
  85. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/__init__.py +0 -0
  86. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/core.py +0 -0
  87. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/proc.py +0 -0
  88. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/unix.py +0 -0
  89. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/local/win.py +0 -0
  90. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/sanitize.py +0 -0
  91. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/__init__.py +0 -0
  92. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/base.py +0 -0
  93. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/session/client.py +0 -0
  94. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/ssh.py +0 -0
  95. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/exec/terminal/tmux.py +0 -0
  96. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/__init__.py +1 -1
  97. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/console/charmap.py +0 -0
  98. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/__init__.py +0 -0
  99. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/bus.py +0 -0
  100. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/events/event.py +0 -0
  101. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/__init__.py +0 -0
  102. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/spinner.py +0 -0
  103. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/spinner/spinner_color.py +0 -0
  104. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/__init__.py +0 -0
  105. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/context.py +0 -0
  106. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/span.py +0 -0
  107. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/tracing/tracer.py +0 -0
  108. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/translate.py +0 -0
  109. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/watcher/__init__.py +0 -0
  110. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/watcher/file_watcher.py +0 -0
  111. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/__init__.py +0 -0
  112. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/application.py +0 -0
  113. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/broker.py +0 -0
  114. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/app.js +0 -0
  115. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  116. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  117. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  118. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  119. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  120. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  121. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  122. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  123. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  124. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  125. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
  126. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.css +0 -0
  127. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/inputbox.js +0 -0
  128. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/style.css +0 -0
  129. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/input_box/voicegif.js +0 -0
  130. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/middleware.py +0 -0
  131. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/stats.py +0 -0
  132. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/media/web/utils.py +0 -0
  133. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/files/__init__.py +0 -0
  134. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/files/file_util.py +0 -0
  135. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/network/__init__.py +0 -0
  136. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/network/http_utils.py +0 -0
  137. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/__init__.py +0 -0
  138. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/base.py +0 -0
  139. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/discovery.py +0 -0
  140. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/registry.py +0 -0
  141. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/plugin/registry_ops.py +0 -0
  142. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/proxy/__init__.py +0 -0
  143. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/proxy/manager.py +0 -0
  144. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/runtime/__init__.py +0 -0
  145. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/runtime/collector.py +0 -0
  146. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/scheduler/__init__.py +0 -0
  147. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/scheduler/scheduler.py +0 -0
  148. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/sdk/__init__.py +0 -0
  149. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/plat/sdk/facade.py +0 -0
  150. {echotools-2.3.38 → echotools-2.3.40}/src/echotools/version.py +0 -0
  151. {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/dependency_links.txt +0 -0
  152. {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/requires.txt +0 -0
  153. {echotools-2.3.38 → echotools-2.3.40}/src/echotools.egg-info/top_level.txt +0 -0
  154. {echotools-2.3.38 → echotools-2.3.40}/src/tests/bench_import.py +0 -0
  155. {echotools-2.3.38 → echotools-2.3.40}/src/tests/conftest.py +0 -0
  156. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_cache_retry.py +0 -0
  157. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_completion.py +0 -0
  158. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_config.py +0 -0
  159. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_config_extended.py +0 -0
  160. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_coverage_core.py +0 -0
  161. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch.py +0 -0
  162. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch_extended.py +0 -0
  163. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_dispatch_race.py +0 -0
  164. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_protocol.py +0 -0
  165. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_thinking_history.py +0 -0
  166. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_entml_tools.py +0 -0
  167. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_errors.py +0 -0
  168. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_errors_common.py +0 -0
  169. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_events.py +0 -0
  170. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_events_event.py +0 -0
  171. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_facade.py +0 -0
  172. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_fncall.py +0 -0
  173. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_ids.py +0 -0
  174. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_imports.py +0 -0
  175. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_io.py +0 -0
  176. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_keys.py +0 -0
  177. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_lifecycle.py +0 -0
  178. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_logger.py +0 -0
  179. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_merge.py +0 -0
  180. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_plugin.py +0 -0
  181. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_printstream.py +0 -0
  182. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_protocols.py +0 -0
  183. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_proxy.py +0 -0
  184. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_proxy_selector.py +0 -0
  185. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_runtime.py +0 -0
  186. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_scheduler.py +0 -0
  187. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_tracing.py +0 -0
  188. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_usage.py +0 -0
  189. {echotools-2.3.38 → echotools-2.3.40}/src/tests/test_web.py +0 -0
  190. {echotools-2.3.38 → 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.38
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.38"
7
+ version = "2.3.40"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -97,6 +97,10 @@ disable_error_code = [
97
97
  module = "echotools.exec.terminal.*"
98
98
  ignore_errors = true
99
99
 
100
+ [[tool.mypy.overrides]]
101
+ module = "echotools.media.console.ui"
102
+ ignore_errors = true
103
+
100
104
  [tool.pytest.ini_options]
101
105
  testpaths = ["src/tests"]
102
106
  asyncio_mode = "auto"
@@ -9,10 +9,13 @@ from typing import Any, Dict, List, Optional, Tuple
9
9
 
10
10
  from echotools.exec.protocol.base import ToolProtocol
11
11
 
12
+
12
13
  # 懒导入:仅 entml 协议需要
13
14
  def _make_thinking_filter(protocol: ToolProtocol):
14
15
  if getattr(protocol, "id", None) == "entml":
15
- from echotools.exec.fncall.protocols.entml_thinking_parse import EntmlThinkingStreamFilter
16
+ from echotools.exec.fncall.protocols.entml_thinking_parse import (
17
+ EntmlThinkingStreamFilter,
18
+ )
16
19
  return EntmlThinkingStreamFilter()
17
20
  return None
18
21
 
@@ -133,12 +136,59 @@ class FncallStreamParser:
133
136
 
134
137
  return buffer, ""
135
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
+
136
174
  def _feed_waiting(self, chunk: str) -> None:
137
175
  """在 WAITING_FOR_TAG 状态下处理新块。"""
138
176
  combined = self._waiting_tail + chunk
139
- 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
140
189
 
141
190
  trigger_tags = self._protocol.get_trigger_tags()
191
+ found, pos = self._protocol.detect_start(combined)
142
192
 
143
193
  if not found:
144
194
  safe, remain = self._split_safe_text(combined, trigger_tags)
@@ -151,7 +201,6 @@ class FncallStreamParser:
151
201
  self._emit_text(combined[:pos])
152
202
 
153
203
  self._fncall_buf = combined[pos:]
154
- self._waiting_tail = ""
155
204
  self._detected = True
156
205
  self._state = self.IN_FUNCTION_CALLS
157
206
 
@@ -213,7 +262,9 @@ class FncallStreamParser:
213
262
 
214
263
  # 兜底:剥离残留 <entml:thinking>(holdback 边界/分片异常时)
215
264
  if self._thinking_filter is not None and clean_text:
216
- from echotools.exec.fncall.protocols.entml_thinking_parse import split_entml_thinking
265
+ from echotools.exec.fncall.protocols.entml_thinking_parse import (
266
+ split_entml_thinking,
267
+ )
217
268
  clean_text, more_thinking = split_entml_thinking(clean_text)
218
269
  if more_thinking:
219
270
  self._thinking_parts.append(more_thinking)
@@ -28,7 +28,10 @@ from echotools.exec.fncall.protocols.entml_thinking_history import (
28
28
  parse_include_thinking_in_history,
29
29
  )
30
30
  from echotools.exec.fncall.shared.loop_detect import detect_tool_loop
31
- from echotools.exec.fncall.shared.normalization import format_tool_descs, normalize_content
31
+ from echotools.exec.fncall.shared.normalization import (
32
+ format_tool_descs,
33
+ normalize_content,
34
+ )
32
35
  from echotools.exec.protocol.base import ToolProtocol
33
36
 
34
37
  __all__ = ["inject_fncall"]
@@ -3,6 +3,10 @@ from __future__ import annotations
3
3
  from typing import Any, Dict, List, Optional, Set, Tuple
4
4
 
5
5
  from echotools.base.logger.manager import get_logger
6
+ from echotools.exec.fncall.protocols.entml_thinking_history import (
7
+ extract_reasoning_text,
8
+ format_entml_thinking_history_block,
9
+ )
6
10
  from echotools.exec.fncall.shared.normalization import normalize_content
7
11
 
8
12
  from .history import (
@@ -15,11 +19,6 @@ from .history import (
15
19
  _try_convert_user_to_tool,
16
20
  )
17
21
 
18
- from echotools.exec.fncall.protocols.entml_thinking_history import (
19
- extract_reasoning_text,
20
- format_entml_thinking_history_block,
21
- )
22
-
23
22
  logger = get_logger(__name__)
24
23
 
25
24
 
@@ -86,7 +85,8 @@ def _format_assistant_text_block(
86
85
  )
87
86
  if not blocks:
88
87
  return None
89
- return f"<assistant>\n{'\n\n'.join(blocks)}\n</assistant>"
88
+ body = "\n\n".join(blocks)
89
+ return f"<assistant>\n{body}\n</assistant>"
90
90
 
91
91
 
92
92
  def _register_tool_call_names(
@@ -15,7 +15,6 @@ from echotools.exec.fncall.prompt.templates import (
15
15
  _HISTORY_TOOL_INVOKE_REMINDER_EN,
16
16
  )
17
17
  from echotools.exec.fncall.protocols.entml_invoke import (
18
- format_entml_tool_calls,
19
18
  parse_entml_tool_calls,
20
19
  )
21
20
  from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
@@ -4,7 +4,6 @@ import json
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
6
  from .entml_patterns import (
7
- BLOCK_RE,
8
7
  INVOKE_RE,
9
8
  PARAM_RE,
10
9
  PARAMETERS_RE,
@@ -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:
@@ -6,13 +6,13 @@ Spinner 动画引擎 - 绿→蓝单向渐变版
6
6
  - stop() 后恢复光标并清除 spinner 行
7
7
  """
8
8
 
9
- import sys
10
- import time
9
+ import math
11
10
  import os
12
11
  import platform
13
- import math
12
+ import sys
14
13
  import threading
15
- from typing import List, Tuple, Dict, Callable, Optional
14
+ import time
15
+ from typing import Callable, Dict, List, Optional, Tuple
16
16
 
17
17
  try:
18
18
  from wcwidth import wcswidth as _wcswidth
@@ -22,12 +22,11 @@ import math
22
22
  import os
23
23
  import re
24
24
  import sys
25
- from pathlib import Path
26
25
  import threading
27
26
  import time
28
- from contextlib import contextmanager
29
27
  from dataclasses import dataclass, field
30
28
  from enum import Enum, auto
29
+ from pathlib import Path
31
30
  from typing import (
32
31
  Any,
33
32
  AsyncIterator,
@@ -51,7 +50,7 @@ from typing import (
51
50
 
52
51
  from rich.console import Console
53
52
  from rich.text import Text
54
- from wcwidth import wcwidth, wcswidth
53
+ from wcwidth import wcswidth, wcwidth
55
54
 
56
55
  # ══════════════════════════════════════════════════════════════════════════════
57
56
  # 类型定义与平台常量
@@ -133,39 +132,39 @@ class _WindowsBackend:
133
132
 
134
133
  def _define_structures(self) -> None:
135
134
  """定义 Windows 控制台输入记录结构体"""
136
- ctypes = self._ctypes
135
+ ct = self._ctypes
137
136
 
138
- class COORD(ctypes.Structure):
139
- _fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]
137
+ class COORD(ct.Structure):
138
+ _fields_ = [("X", ct.c_short), ("Y", ct.c_short)]
140
139
 
141
- class KEY_EVENT_RECORD(ctypes.Structure):
140
+ class KEY_EVENT_RECORD(ct.Structure):
142
141
  _fields_ = [
143
- ("bKeyDown", ctypes.wintypes.BOOL),
144
- ("wRepeatCount", ctypes.wintypes.WORD),
145
- ("wVirtualKeyCode", ctypes.wintypes.WORD),
146
- ("wVirtualScanCode", ctypes.wintypes.WORD),
147
- ("uChar", ctypes.wintypes.WCHAR),
148
- ("dwControlKeyState", ctypes.wintypes.DWORD),
142
+ ("bKeyDown", ct.wintypes.BOOL),
143
+ ("wRepeatCount", ct.wintypes.WORD),
144
+ ("wVirtualKeyCode", ct.wintypes.WORD),
145
+ ("wVirtualScanCode", ct.wintypes.WORD),
146
+ ("uChar", ct.wintypes.WCHAR),
147
+ ("dwControlKeyState", ct.wintypes.DWORD),
149
148
  ]
150
149
 
151
- class MOUSE_EVENT_RECORD(ctypes.Structure):
150
+ class MOUSE_EVENT_RECORD(ct.Structure):
152
151
  _fields_ = [
153
152
  ("dwMousePosition", COORD),
154
- ("dwButtonState", ctypes.wintypes.DWORD),
155
- ("dwControlKeyState", ctypes.wintypes.DWORD),
156
- ("dwEventFlags", ctypes.wintypes.DWORD),
153
+ ("dwButtonState", ct.wintypes.DWORD),
154
+ ("dwControlKeyState", ct.wintypes.DWORD),
155
+ ("dwEventFlags", ct.wintypes.DWORD),
157
156
  ]
158
157
 
159
- class WINDOW_BUFFER_SIZE_RECORD(ctypes.Structure):
158
+ class WINDOW_BUFFER_SIZE_RECORD(ct.Structure):
160
159
  _fields_ = [("dwSize", COORD)]
161
160
 
162
- class MENU_EVENT_RECORD(ctypes.Structure):
163
- _fields_ = [("dwCommandId", ctypes.wintypes.UINT)]
161
+ class MENU_EVENT_RECORD(ct.Structure):
162
+ _fields_ = [("dwCommandId", ct.wintypes.UINT)]
164
163
 
165
- class FOCUS_EVENT_RECORD(ctypes.Structure):
166
- _fields_ = [("bSetFocus", ctypes.wintypes.BOOL)]
164
+ class FOCUS_EVENT_RECORD(ct.Structure):
165
+ _fields_ = [("bSetFocus", ct.wintypes.BOOL)]
167
166
 
168
- class INPUT_RECORD_EVENT(ctypes.Union):
167
+ class INPUT_RECORD_EVENT(ct.Union):
169
168
  _fields_ = [
170
169
  ("KeyEvent", KEY_EVENT_RECORD),
171
170
  ("MouseEvent", MOUSE_EVENT_RECORD),
@@ -174,9 +173,9 @@ class _WindowsBackend:
174
173
  ("FocusEvent", FOCUS_EVENT_RECORD),
175
174
  ]
176
175
 
177
- class INPUT_RECORD(ctypes.Structure):
176
+ class INPUT_RECORD(ct.Structure):
178
177
  _fields_ = [
179
- ("EventType", ctypes.wintypes.WORD),
178
+ ("EventType", ct.wintypes.WORD),
180
179
  ("Event", INPUT_RECORD_EVENT),
181
180
  ]
182
181
 
@@ -304,8 +303,8 @@ class _UnixBackend:
304
303
  vk = arrow_map.get(ch3, "unknown")
305
304
  # 消费 Delete 键的尾部 '~'
306
305
  if ch3 == "3":
307
- extra, _, _ = select.select([fd], [], [], 0.02)
308
- if extra:
306
+ pending_del, _, _ = select.select([fd], [], [], 0.02)
307
+ if pending_del:
309
308
  os.read(fd, 1)
310
309
  results.append({
311
310
  "type": "key", "vk": vk,
@@ -389,10 +388,10 @@ class _UnixBackend:
389
388
  # 尝试读取更多字节以组成完整 UTF-8 字符
390
389
  more, _, _ = select.select([fd], [], [], 0.02)
391
390
  if more:
392
- extra = os.read(fd, 3).decode(
391
+ utf_tail = os.read(fd, 3).decode(
393
392
  "utf-8", errors="replace",
394
393
  )
395
- full_char += extra
394
+ full_char += utf_tail
396
395
  results.append({
397
396
  "type": "key", "vk": "char",
398
397
  "char": full_char, "ctrl": False,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.38
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
@@ -8,8 +8,6 @@ def test_console_package_import() -> None:
8
8
  BorderStyle,
9
9
  Clock,
10
10
  ConsoleUI,
11
- FontStyle,
12
- GradientTheme,
13
11
  Spinner,
14
12
  char_map,
15
13
  )
@@ -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