echotools 2.3.40__tar.gz → 2.3.43__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 (192) hide show
  1. {echotools-2.3.40 → echotools-2.3.43}/PKG-INFO +1 -1
  2. {echotools-2.3.40 → echotools-2.3.43}/pyproject.toml +1 -1
  3. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/stream.py +3 -3
  4. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/history.py +39 -0
  5. echotools-2.3.43/src/echotools/exec/fncall/prompt/history_format.py +116 -0
  6. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/inject.py +6 -5
  7. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/prompt_helpers.py +1 -1
  8. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml.py +29 -6
  9. echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/__init__.py +31 -0
  10. echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking.py → echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/core.py +21 -10
  11. echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_parse.py → echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/parse.py +47 -38
  12. {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/PKG-INFO +1 -1
  13. {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/SOURCES.txt +4 -4
  14. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_protocol.py +47 -14
  15. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_thinking_history.py +1 -1
  16. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_thinking_parse.py +1 -1
  17. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_fncall_stream_thinking.py +1 -1
  18. echotools-2.3.40/src/echotools/exec/fncall/prompt/history_format.py +0 -89
  19. echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_sanitize.py +0 -24
  20. {echotools-2.3.40 → echotools-2.3.43}/README.md +0 -0
  21. {echotools-2.3.40 → echotools-2.3.43}/docs/README.md +0 -0
  22. {echotools-2.3.40 → echotools-2.3.43}/setup.cfg +0 -0
  23. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/__init__.py +0 -0
  24. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/__init__.py +0 -0
  25. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/list_cache.py +0 -0
  26. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/cache/memory_cache.py +0 -0
  27. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/__init__.py +0 -0
  28. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/base.py +0 -0
  29. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center.py +0 -0
  30. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center_notify.py +0 -0
  31. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/center_template.py +0 -0
  32. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/loader.py +0 -0
  33. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/config/merge.py +0 -0
  34. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/__init__.py +0 -0
  35. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/base.py +0 -0
  36. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/classify.py +0 -0
  37. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/common.py +0 -0
  38. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/errors/http.py +0 -0
  39. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/ids/__init__.py +0 -0
  40. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/ids/generator.py +0 -0
  41. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/__init__.py +0 -0
  42. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/io_utils.py +0 -0
  43. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/io/printstream.py +0 -0
  44. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/logger/__init__.py +0 -0
  45. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/logger/manager.py +0 -0
  46. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/retry/__init__.py +0 -0
  47. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/base/retry/retry.py +0 -0
  48. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/compat.py +0 -0
  49. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/__init__.py +0 -0
  50. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/candidate.py +0 -0
  51. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  52. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  53. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/race.py +0 -0
  54. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/selector.py +0 -0
  55. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/dispatch/usage.py +0 -0
  56. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/__init__.py +0 -0
  57. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  58. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  59. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  60. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  61. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  62. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  63. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  64. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  65. /echotools-2.3.40/src/echotools/exec/fncall/protocols/entml_thinking_history.py → /echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  66. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  67. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  68. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/registry.py +0 -0
  69. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  70. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  71. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  72. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  73. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  74. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  75. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/keys.py +0 -0
  76. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/__init__.py +0 -0
  77. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/manager.py +0 -0
  78. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/updater.py +0 -0
  79. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  80. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/process/__init__.py +0 -0
  81. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/process/port.py +0 -0
  82. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/protocol/__init__.py +0 -0
  83. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/protocol/base.py +0 -0
  84. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/__init__.py +0 -0
  85. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/conpty.py +0 -0
  86. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/__init__.py +0 -0
  87. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/core.py +0 -0
  88. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/proc.py +0 -0
  89. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/unix.py +0 -0
  90. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/local/win.py +0 -0
  91. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/sanitize.py +0 -0
  92. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/__init__.py +0 -0
  93. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/base.py +0 -0
  94. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/session/client.py +0 -0
  95. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/ssh.py +0 -0
  96. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/exec/terminal/tmux.py +0 -0
  97. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/__init__.py +0 -0
  98. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/charmap.py +0 -0
  99. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/spinner.py +0 -0
  100. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/console/ui.py +0 -0
  101. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/__init__.py +0 -0
  102. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/bus.py +0 -0
  103. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/events/event.py +0 -0
  104. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/__init__.py +0 -0
  105. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/spinner.py +0 -0
  106. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/spinner/spinner_color.py +0 -0
  107. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/__init__.py +0 -0
  108. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/context.py +0 -0
  109. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/span.py +0 -0
  110. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/tracing/tracer.py +0 -0
  111. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/translate.py +0 -0
  112. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/watcher/__init__.py +0 -0
  113. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/watcher/file_watcher.py +0 -0
  114. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/__init__.py +0 -0
  115. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/application.py +0 -0
  116. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/broker.py +0 -0
  117. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/app.js +0 -0
  118. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  119. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  120. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  121. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  122. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  123. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  124. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  125. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  126. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  127. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  128. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
  129. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/inputbox.css +0 -0
  130. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/inputbox.js +0 -0
  131. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/style.css +0 -0
  132. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/input_box/voicegif.js +0 -0
  133. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/middleware.py +0 -0
  134. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/stats.py +0 -0
  135. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/media/web/utils.py +0 -0
  136. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/files/__init__.py +0 -0
  137. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/files/file_util.py +0 -0
  138. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/network/__init__.py +0 -0
  139. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/network/http_utils.py +0 -0
  140. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/__init__.py +0 -0
  141. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/base.py +0 -0
  142. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/discovery.py +0 -0
  143. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/registry.py +0 -0
  144. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/plugin/registry_ops.py +0 -0
  145. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/proxy/__init__.py +0 -0
  146. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/proxy/manager.py +0 -0
  147. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/runtime/__init__.py +0 -0
  148. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/runtime/collector.py +0 -0
  149. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/scheduler/__init__.py +0 -0
  150. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/scheduler/scheduler.py +0 -0
  151. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/sdk/__init__.py +0 -0
  152. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/plat/sdk/facade.py +0 -0
  153. {echotools-2.3.40 → echotools-2.3.43}/src/echotools/version.py +0 -0
  154. {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/dependency_links.txt +0 -0
  155. {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/requires.txt +0 -0
  156. {echotools-2.3.40 → echotools-2.3.43}/src/echotools.egg-info/top_level.txt +0 -0
  157. {echotools-2.3.40 → echotools-2.3.43}/src/tests/bench_import.py +0 -0
  158. {echotools-2.3.40 → echotools-2.3.43}/src/tests/conftest.py +0 -0
  159. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_cache_retry.py +0 -0
  160. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_completion.py +0 -0
  161. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_config.py +0 -0
  162. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_config_extended.py +0 -0
  163. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_console.py +0 -0
  164. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_coverage_core.py +0 -0
  165. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch.py +0 -0
  166. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch_extended.py +0 -0
  167. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_dispatch_race.py +0 -0
  168. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_entml_tools.py +0 -0
  169. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_errors.py +0 -0
  170. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_errors_common.py +0 -0
  171. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_events.py +0 -0
  172. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_events_event.py +0 -0
  173. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_facade.py +0 -0
  174. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_fncall.py +0 -0
  175. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_ids.py +0 -0
  176. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_imports.py +0 -0
  177. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_io.py +0 -0
  178. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_keys.py +0 -0
  179. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_lifecycle.py +0 -0
  180. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_logger.py +0 -0
  181. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_merge.py +0 -0
  182. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_plugin.py +0 -0
  183. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_printstream.py +0 -0
  184. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_protocols.py +0 -0
  185. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_proxy.py +0 -0
  186. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_proxy_selector.py +0 -0
  187. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_runtime.py +0 -0
  188. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_scheduler.py +0 -0
  189. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_tracing.py +0 -0
  190. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_usage.py +0 -0
  191. {echotools-2.3.40 → echotools-2.3.43}/src/tests/test_web.py +0 -0
  192. {echotools-2.3.40 → echotools-2.3.43}/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.40
3
+ Version: 2.3.43
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.40"
7
+ version = "2.3.43"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -13,7 +13,7 @@ from echotools.exec.protocol.base import ToolProtocol
13
13
  # 懒导入:仅 entml 协议需要
14
14
  def _make_thinking_filter(protocol: ToolProtocol):
15
15
  if getattr(protocol, "id", None) == "entml":
16
- from echotools.exec.fncall.protocols.entml_thinking_parse import (
16
+ from echotools.exec.fncall.protocols.entml_think.parse import (
17
17
  EntmlThinkingStreamFilter,
18
18
  )
19
19
  return EntmlThinkingStreamFilter()
@@ -177,7 +177,7 @@ class FncallStreamParser:
177
177
  self._waiting_tail = ""
178
178
 
179
179
  if self._thinking_filter is not None:
180
- from echotools.exec.fncall.protocols.entml_thinking_parse import (
180
+ from echotools.exec.fncall.protocols.entml_think.parse import (
181
181
  has_unclosed_entml_thinking,
182
182
  )
183
183
  if (
@@ -262,7 +262,7 @@ class FncallStreamParser:
262
262
 
263
263
  # 兜底:剥离残留 <entml:thinking>(holdback 边界/分片异常时)
264
264
  if self._thinking_filter is not None and clean_text:
265
- from echotools.exec.fncall.protocols.entml_thinking_parse import (
265
+ from echotools.exec.fncall.protocols.entml_think.parse import (
266
266
  split_entml_thinking,
267
267
  )
268
268
  clean_text, more_thinking = split_entml_thinking(clean_text)
@@ -39,6 +39,45 @@ _TOOL_CALL_MARKER_RE = re.compile(
39
39
  re.MULTILINE,
40
40
  )
41
41
 
42
+ # entml 标签清理正则
43
+ _ENTML_FNCALL_BLOCK_RE = re.compile(
44
+ r"<entml:function_calls\b[^>]*>.*?</entml:function_calls>",
45
+ re.DOTALL,
46
+ )
47
+ _ENTML_INVOKE_RE = re.compile(
48
+ r'<entml:invoke\s+name="[^"]+"\s*>.*?</entml:invoke>',
49
+ re.DOTALL,
50
+ )
51
+ _ENTML_PARAMETERS_RE = re.compile(
52
+ r'<entml:parameters>.*?</entml:parameters>',
53
+ re.DOTALL,
54
+ )
55
+ _ENTML_PARAM_RE = re.compile(
56
+ r'<entml:parameter\s+name="[^"]+"[^>]*>.*?</entml:parameter>',
57
+ re.DOTALL,
58
+ )
59
+ # 清理任意 entml:* 标签
60
+ _ENTML_ANY_TAG_RE = re.compile(
61
+ r'<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>',
62
+ re.DOTALL,
63
+ )
64
+
65
+
66
+ def _clean_entml_tags(content: str) -> str:
67
+ """移除内容中的所有 entml 标签及其内部内容。"""
68
+ if not content:
69
+ return content
70
+ cleaned = content
71
+ cleaned = _ENTML_FNCALL_BLOCK_RE.sub("", cleaned)
72
+ cleaned = _ENTML_INVOKE_RE.sub("", cleaned)
73
+ cleaned = _ENTML_PARAMETERS_RE.sub("", cleaned)
74
+ cleaned = _ENTML_PARAM_RE.sub("", cleaned)
75
+ cleaned = _ENTML_ANY_TAG_RE.sub("", cleaned)
76
+ # 清理多余空行
77
+ lines = [line for line in cleaned.splitlines() if line.strip()]
78
+ return "\n".join(lines)
79
+
80
+
42
81
  # ---------------------------------------------------------------------------
43
82
  # 渲染工具调用 / 工具结果
44
83
  # ---------------------------------------------------------------------------
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ """Conversation history block formatting."""
4
+
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ from echotools.exec.fncall.shared.normalization import normalize_content
8
+
9
+ from .prompt_helpers import (
10
+ format_assistant_block,
11
+ format_assistant_block_with_results,
12
+ format_tool_block,
13
+ format_user_block,
14
+ join_history_parts,
15
+ )
16
+
17
+
18
+ def history_contains_tool_calls(messages: List[Dict[str, Any]]) -> bool:
19
+ """历史消息中是否包含 assistant tool_calls 或 tool 结果。"""
20
+ for m in messages:
21
+ role = m.get("role") or "user"
22
+ if role == "assistant" and m.get("tool_calls"):
23
+ return True
24
+ if role == "tool":
25
+ return True
26
+ return False
27
+
28
+
29
+ def _append_user_part(
30
+ parts: List[Tuple[str, bool]],
31
+ m: Dict[str, Any],
32
+ clean_fn: Any,
33
+ ) -> None:
34
+ content_str = normalize_content(m.get("content", ""))
35
+ parts.append((format_user_block(content_str, clean_fn), False))
36
+
37
+
38
+ def _append_assistant_part(
39
+ parts: List[Tuple[str, bool]],
40
+ messages: List[Dict[str, Any]],
41
+ index: int,
42
+ protocol: Optional[Any],
43
+ call_id_to_name: Dict[str, str],
44
+ seen_assistant_keys: set,
45
+ *,
46
+ include_thinking_in_history: bool,
47
+ ) -> int:
48
+ m = messages[index]
49
+ content_str = normalize_content(m.get("content", ""))
50
+ tcs = m.get("tool_calls") or []
51
+ if tcs:
52
+ tool_msgs = []
53
+ j = index + 1
54
+ while j < len(messages) and (messages[j].get("role") or "user") == "tool":
55
+ tool_msgs.append(messages[j])
56
+ j += 1
57
+ blocks = format_assistant_block_with_results(
58
+ m, tool_msgs, protocol, call_id_to_name, seen_assistant_keys,
59
+ include_thinking_in_history=include_thinking_in_history,
60
+ )
61
+ for block in blocks:
62
+ parts.append((block, False))
63
+ return j
64
+
65
+ blocks = format_assistant_block(
66
+ m, content_str, protocol, call_id_to_name, seen_assistant_keys,
67
+ include_thinking_in_history=include_thinking_in_history,
68
+ )
69
+ for block in blocks:
70
+ parts.append((block, False))
71
+ return index + 1
72
+
73
+
74
+ def _format_conversation_history(
75
+ messages: List[Dict[str, Any]],
76
+ protocol: Optional[Any] = None,
77
+ *,
78
+ include_thinking_in_history: bool = False,
79
+ ) -> str:
80
+ if not messages:
81
+ return ""
82
+
83
+ call_id_to_name: Dict[str, str] = {}
84
+ seen_assistant_keys = set()
85
+ parts: List[Tuple[str, bool]] = []
86
+ clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
87
+
88
+ i = 0
89
+ while i < len(messages):
90
+ m = messages[i]
91
+ role: str = m.get("role") or "user"
92
+
93
+ if role == "user":
94
+ _append_user_part(parts, m, clean_fn)
95
+ i += 1
96
+ continue
97
+
98
+ if role == "assistant":
99
+ i = _append_assistant_part(
100
+ parts, messages, i, protocol, call_id_to_name,
101
+ seen_assistant_keys,
102
+ include_thinking_in_history=include_thinking_in_history,
103
+ )
104
+ continue
105
+
106
+ if role == "tool":
107
+ content_str = normalize_content(m.get("content", ""))
108
+ parts.append((format_tool_block(m, content_str, call_id_to_name), True))
109
+ i += 1
110
+ continue
111
+
112
+ content_str = normalize_content(m.get("content", ""))
113
+ parts.append((f"<{role}>\n{content_str}\n</{role}>", False))
114
+ i += 1
115
+
116
+ return join_history_parts(parts)
@@ -22,8 +22,8 @@ from echotools.exec.fncall.protocols.entml import (
22
22
  format_entml_conversation_history,
23
23
  format_entml_current_user_message,
24
24
  )
25
- from echotools.exec.fncall.protocols.entml_thinking import build_entml_thinking_section
26
- from echotools.exec.fncall.protocols.entml_thinking_history import (
25
+ from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
26
+ from echotools.exec.fncall.protocols.entml_think.hist import (
27
27
  apply_thinking_history_policy,
28
28
  parse_include_thinking_in_history,
29
29
  )
@@ -145,12 +145,13 @@ def inject_fncall(
145
145
  if not tools:
146
146
  if protocol.id == "entml":
147
147
  sections: List[str] = []
148
- thinking_section = build_entml_thinking_section(protocol_options)
149
- if thinking_section:
150
- sections.append(thinking_section)
151
148
  if history_text.strip():
152
149
  sections.append(format_entml_conversation_history(history_text))
153
150
  sections.append(format_entml_current_user_message(current_user_message))
151
+ # thinking 放在最后,超限截断时优先保留在 send_text 尾部
152
+ thinking_section = build_entml_thinking_section(protocol_options)
153
+ if thinking_section:
154
+ sections.append(thinking_section)
154
155
  prompt = "\n\n".join(sections)
155
156
  else:
156
157
  prompt = build_no_tools_prompt(history_text, current_user_message)
@@ -3,7 +3,7 @@ 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 (
6
+ from echotools.exec.fncall.protocols.entml_think.hist import (
7
7
  extract_reasoning_text,
8
8
  format_entml_thinking_history_block,
9
9
  )
@@ -8,6 +8,7 @@
8
8
  from __future__ import annotations
9
9
 
10
10
  import json as _json
11
+ import re
11
12
  from typing import Any, Dict, List, Optional, Tuple
12
13
 
13
14
  from echotools.exec.fncall.prompt.templates import (
@@ -18,8 +19,7 @@ from echotools.exec.fncall.protocols.entml_invoke import (
18
19
  parse_entml_tool_calls,
19
20
  )
20
21
  from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
21
- from echotools.exec.fncall.protocols.entml_sanitize import strip_entml_from_content
22
- from echotools.exec.fncall.protocols.entml_thinking import build_entml_thinking_section
22
+ from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
23
23
  from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
24
24
  from echotools.exec.fncall.shared.coercion import _build_param_schema_index
25
25
  from echotools.exec.fncall.shared.normalization import (
@@ -28,6 +28,27 @@ from echotools.exec.fncall.shared.normalization import (
28
28
  )
29
29
  from echotools.exec.protocol.base import ToolProtocol
30
30
 
31
+ # 仅按 entml: 前缀剥离标签,不区分具体标签名。
32
+ _ENTML_PAIR_RE = re.compile(
33
+ r"<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>",
34
+ re.DOTALL,
35
+ )
36
+ _ENTML_SELF_CLOSING_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*/>", re.DOTALL)
37
+ _ENTML_ORPHAN_CLOSE_RE = re.compile(r"</entml:[a-zA-Z_][\w]*>", re.DOTALL)
38
+ _ENTML_ORPHAN_OPEN_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*>", re.DOTALL)
39
+
40
+
41
+ def strip_entml_from_content(content: str) -> str:
42
+ """从 user 消息正文剥离所有 entml:* 标签及残留开闭标签。"""
43
+ if not content:
44
+ return content
45
+ cleaned = content
46
+ cleaned = _ENTML_PAIR_RE.sub("", cleaned)
47
+ cleaned = _ENTML_SELF_CLOSING_RE.sub("", cleaned)
48
+ cleaned = _ENTML_ORPHAN_CLOSE_RE.sub("", cleaned)
49
+ cleaned = _ENTML_ORPHAN_OPEN_RE.sub("", cleaned)
50
+ return cleaned.strip()
51
+
31
52
 
32
53
  def _parse_tool_call_args(tc: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
33
54
  fn = tc.get("function") or {}
@@ -141,10 +162,6 @@ class EntmlProtocol(ToolProtocol):
141
162
  else:
142
163
  sections = [_ENTML_INSTRUCTION.rstrip()]
143
164
 
144
- thinking_section = build_entml_thinking_section(protocol_options)
145
- if thinking_section:
146
- sections.append(thinking_section)
147
-
148
165
  if user_system_prompt and user_system_prompt.strip():
149
166
  sections.append(
150
167
  f"<user_system_prompt>\n{user_system_prompt.strip()}\n</user_system_prompt>"
@@ -161,6 +178,12 @@ class EntmlProtocol(ToolProtocol):
161
178
  sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
162
179
 
163
180
  sections.append(format_entml_current_user_message(current_user_message))
181
+
182
+ # thinking 放在最后,超限截断时优先保留在 send_text 尾部
183
+ thinking_section = build_entml_thinking_section(protocol_options)
184
+ if thinking_section:
185
+ sections.append(thinking_section)
186
+
164
187
  return "\n\n".join(sections)
165
188
 
166
189
  def detect_start(self, buffer: str) -> Tuple[bool, int]:
@@ -0,0 +1,31 @@
1
+ """Entml thinking: prompt section, history blocks, stream parsing."""
2
+
3
+ from echotools.exec.fncall.protocols.entml_think.core import (
4
+ build_entml_thinking_section,
5
+ parse_max_thinking_length,
6
+ )
7
+ from echotools.exec.fncall.protocols.entml_think.hist import (
8
+ apply_thinking_history_policy,
9
+ extract_reasoning_text,
10
+ format_entml_thinking_history_block,
11
+ parse_include_thinking_in_history,
12
+ parse_interleaved_history,
13
+ )
14
+ from echotools.exec.fncall.protocols.entml_think.parse import (
15
+ EntmlThinkingStreamFilter,
16
+ has_unclosed_entml_thinking,
17
+ split_entml_thinking,
18
+ )
19
+
20
+ __all__ = [
21
+ "EntmlThinkingStreamFilter",
22
+ "apply_thinking_history_policy",
23
+ "build_entml_thinking_section",
24
+ "extract_reasoning_text",
25
+ "format_entml_thinking_history_block",
26
+ "has_unclosed_entml_thinking",
27
+ "parse_include_thinking_in_history",
28
+ "parse_interleaved_history",
29
+ "parse_max_thinking_length",
30
+ "split_entml_thinking",
31
+ ]
@@ -53,11 +53,11 @@ _INJECTION_MODE_ALIASES: Dict[str, str] = {
53
53
  }
54
54
 
55
55
  _DEFAULT_MAX_BY_LEVEL: Dict[str, int] = {
56
- "low": 6554,
57
- "medium": 13108,
58
- "high": 32768,
59
- "xhigh": 52428,
60
- "max": 62260,
56
+ "low": 12800,
57
+ "medium": 25600,
58
+ "high": 64000,
59
+ "xhigh": 102400,
60
+ "max": 134736,
61
61
  }
62
62
 
63
63
  _LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
@@ -120,9 +120,12 @@ def parse_max_thinking_length(value: Any) -> Optional[int]:
120
120
  def resolve_thinking_injection(
121
121
  protocol_options: Optional[Dict[str, Any]] = None,
122
122
  ) -> Optional[Tuple[str, Optional[int]]]:
123
- """解析注入用 thinking_mode (on/auto) 与 max_thinking_length。
123
+ """解析注入用 thinking_mode 与 max_thinking_length。
124
124
 
125
- 返回 None 表示不注入任何思考相关内容(none / off)。
125
+ - thinking_level 为 low|medium|high|xhigh|max 时,thinking_mode 直接传该挡位名。
126
+ - 仅 thinking_mode=on(无 level)时用 ``on``,并按 medium 默认长度。
127
+ - auto 传 ``auto``,无默认 max_thinking_length。
128
+ - none / off 返回 None,不注入任何思考相关内容。
126
129
  """
127
130
  opts = protocol_options or {}
128
131
 
@@ -130,8 +133,12 @@ def resolve_thinking_injection(
130
133
  if level is not None:
131
134
  if level == "none":
132
135
  return None
133
- injection_mode = "auto" if level == "auto" else "on"
134
- default_max = default_max_thinking_length_for_level(level)
136
+ if level == "auto":
137
+ injection_mode = "auto"
138
+ default_max = None
139
+ else:
140
+ injection_mode = level
141
+ default_max = default_max_thinking_length_for_level(level)
135
142
  else:
136
143
  mode = normalize_thinking_mode(opts.get("thinking_mode"))
137
144
  if mode is None or mode == "off":
@@ -144,8 +151,12 @@ def resolve_thinking_injection(
144
151
  return injection_mode, max_length
145
152
 
146
153
 
154
+ def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
155
+ return injection_mode != "auto"
156
+
157
+
147
158
  def _format_thinking_behavior(injection_mode: str) -> str:
148
- body = _THINKING_BEHAVIOR_ON if injection_mode == "on" else _THINKING_BEHAVIOR_AUTO
159
+ body = _THINKING_BEHAVIOR_ON if _uses_forced_thinking_behavior(injection_mode) else _THINKING_BEHAVIOR_AUTO
149
160
  return f"<thinking_behavior>\n{body}\n</thinking_behavior>"
150
161
 
151
162
 
@@ -92,6 +92,49 @@ class EntmlThinkingStreamFilter:
92
92
  return True
93
93
  return False
94
94
 
95
+ def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
96
+ """处理块内 pending;返回 False 表示应退出 feed 主循环。"""
97
+ close_at = self._pending.find(_THINKING_CLOSE)
98
+ if close_at >= 0:
99
+ piece = self._pending[:close_at]
100
+ emitted = self._emit_thinking_piece(piece)
101
+ if emitted:
102
+ out.append(("thinking", emitted))
103
+ self._in_block = False
104
+ self._thinking_started = False
105
+ self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
106
+ return True
107
+
108
+ safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
109
+ if safe:
110
+ emitted = self._emit_thinking_piece(safe)
111
+ if emitted:
112
+ out.append(("thinking", emitted))
113
+ self._pending = hold
114
+ return False
115
+
116
+ def _feed_before_block(self, out: List[Tuple[str, str]]) -> bool:
117
+ """开标签之前或尚未进入块;返回 False 表示应退出 feed 主循环。"""
118
+ open_at = self._pending.find(_THINKING_OPEN_PREFIX)
119
+ if open_at < 0:
120
+ safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
121
+ if safe:
122
+ out.append(("content", safe))
123
+ return False
124
+
125
+ if open_at > 0:
126
+ out.append(("content", self._pending[:open_at]))
127
+ self._pending = self._pending[open_at:]
128
+
129
+ gt = self._pending.find(">")
130
+ if gt < 0:
131
+ return False
132
+
133
+ self._in_block = True
134
+ self._thinking_started = False
135
+ self._pending = self._pending[gt + 1 :]
136
+ return True
137
+
95
138
  def feed(self, chunk: str) -> List[Tuple[str, str]]:
96
139
  """返回 [(kind, text), ...],kind 为 content 或 thinking。"""
97
140
  if not chunk:
@@ -102,46 +145,12 @@ class EntmlThinkingStreamFilter:
102
145
 
103
146
  while self._pending:
104
147
  if self._in_block:
105
- close_at = self._pending.find(_THINKING_CLOSE)
106
- if close_at >= 0:
107
- piece = self._pending[:close_at]
108
- emitted = self._emit_thinking_piece(piece)
109
- if emitted:
110
- out.append(("thinking", emitted))
111
- self._in_block = False
112
- self._thinking_started = False
113
- self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
114
- continue
115
-
116
- # 关闭标签可能被分片:hold 真前缀,其余立即作为 thinking 输出
117
- safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
118
- if safe:
119
- emitted = self._emit_thinking_piece(safe)
120
- if emitted:
121
- out.append(("thinking", emitted))
122
- self._pending = hold
123
- break
124
-
125
- open_at = self._pending.find(_THINKING_OPEN_PREFIX)
126
- if open_at < 0:
127
- safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
128
- if safe:
129
- out.append(("content", safe))
148
+ if not self._feed_in_block(out):
149
+ break
150
+ continue
151
+ if not self._feed_before_block(out):
130
152
  break
131
153
 
132
- if open_at > 0:
133
- out.append(("content", self._pending[:open_at]))
134
- self._pending = self._pending[open_at:]
135
-
136
- gt = self._pending.find(">")
137
- if gt < 0:
138
- # 开标签属性未收齐
139
- break
140
-
141
- self._in_block = True
142
- self._thinking_started = False
143
- self._pending = self._pending[gt + 1 :]
144
-
145
154
  return out
146
155
 
147
156
  def _emit_thinking_piece(self, piece: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.40
3
+ Version: 2.3.43
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -57,12 +57,12 @@ src/echotools/exec/fncall/protocols/__init__.py
57
57
  src/echotools/exec/fncall/protocols/entml.py
58
58
  src/echotools/exec/fncall/protocols/entml_invoke.py
59
59
  src/echotools/exec/fncall/protocols/entml_patterns.py
60
- src/echotools/exec/fncall/protocols/entml_sanitize.py
61
- src/echotools/exec/fncall/protocols/entml_thinking.py
62
- src/echotools/exec/fncall/protocols/entml_thinking_history.py
63
- src/echotools/exec/fncall/protocols/entml_thinking_parse.py
64
60
  src/echotools/exec/fncall/protocols/entml_tools.py
65
61
  src/echotools/exec/fncall/protocols/entml_values.py
62
+ src/echotools/exec/fncall/protocols/entml_think/__init__.py
63
+ src/echotools/exec/fncall/protocols/entml_think/core.py
64
+ src/echotools/exec/fncall/protocols/entml_think/hist.py
65
+ src/echotools/exec/fncall/protocols/entml_think/parse.py
66
66
  src/echotools/exec/fncall/shared/__init__.py
67
67
  src/echotools/exec/fncall/shared/coercion.py
68
68
  src/echotools/exec/fncall/shared/loop_detect.py
@@ -6,7 +6,7 @@ import pytest
6
6
 
7
7
  from echotools.exec.fncall import get_protocol, inject_fncall
8
8
  from echotools.exec.fncall.protocols.entml_invoke import parse_entml_tool_calls
9
- from echotools.exec.fncall.protocols.entml_thinking import (
9
+ from echotools.exec.fncall.protocols.entml_think.core import (
10
10
  build_entml_thinking_section,
11
11
  default_max_thinking_length_for_level,
12
12
  normalize_thinking_level,
@@ -77,7 +77,7 @@ def test_build_entml_thinking_section_empty_without_options() -> None:
77
77
 
78
78
 
79
79
  def test_parse_max_thinking_length() -> None:
80
- from echotools.exec.fncall.protocols.entml_thinking import parse_max_thinking_length
80
+ from echotools.exec.fncall.protocols.entml_think.core import parse_max_thinking_length
81
81
 
82
82
  assert parse_max_thinking_length(None) is None
83
83
  assert parse_max_thinking_length("") is None
@@ -128,11 +128,11 @@ def test_normalize_thinking_level(raw, expected) -> None:
128
128
  @pytest.mark.parametrize(
129
129
  "level,expected",
130
130
  [
131
- ("low", 6554),
132
- ("medium", 13108),
133
- ("high", 32768),
134
- ("xhigh", 52428),
135
- ("max", 62260),
131
+ ("low", 12800),
132
+ ("medium", 25600),
133
+ ("high", 64000),
134
+ ("xhigh", 102400),
135
+ ("max", 134736),
136
136
  ("none", None),
137
137
  ("auto", None),
138
138
  ],
@@ -143,13 +143,13 @@ def test_default_max_thinking_length_for_level(level, expected) -> None:
143
143
 
144
144
  def test_resolve_thinking_injection_levels() -> None:
145
145
  assert resolve_thinking_injection({"thinking_level": "none"}) is None
146
- assert resolve_thinking_injection({"thinking_level": "low"}) == ("on", 6554)
147
- assert resolve_thinking_injection({"thinking_level": "high"}) == ("on", 32768)
146
+ assert resolve_thinking_injection({"thinking_level": "low"}) == ("low", 12800)
147
+ assert resolve_thinking_injection({"thinking_level": "high"}) == ("high", 64000)
148
148
  assert resolve_thinking_injection({"thinking_level": "auto"}) == ("auto", None)
149
149
  assert resolve_thinking_injection({
150
150
  "thinking_level": "max",
151
151
  "max_thinking_length": 1000,
152
- }) == ("on", 1000)
152
+ }) == ("max", 1000)
153
153
 
154
154
 
155
155
  def test_thinking_prompt_off() -> None:
@@ -163,18 +163,24 @@ def test_thinking_prompt_off() -> None:
163
163
  def test_thinking_prompt_on_without_max_length() -> None:
164
164
  section = build_entml_thinking_section({"thinking_mode": "on"})
165
165
  assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
166
- assert "<entml:max_thinking_length>13108</entml:max_thinking_length>" in section
166
+ assert "<entml:max_thinking_length>25600</entml:max_thinking_length>" in section
167
167
  assert "<thinking_behavior>" in section
168
168
 
169
169
 
170
170
  def test_thinking_prompt_on_by_level() -> None:
171
171
  section = build_entml_thinking_section({"thinking_level": "low"})
172
- assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
173
- assert "<entml:max_thinking_length>6554</entml:max_thinking_length>" in section
172
+ assert "<entml:thinking_mode>low</entml:thinking_mode>" in section
173
+ assert "<entml:max_thinking_length>12800</entml:max_thinking_length>" in section
174
174
  assert "Never skip the thinking block" in section
175
175
  assert "Your default is to think before it answers" in section
176
176
 
177
177
 
178
+ def test_thinking_prompt_medium_by_level() -> None:
179
+ section = build_entml_thinking_section({"thinking_level": "medium"})
180
+ assert "<entml:thinking_mode>medium</entml:thinking_mode>" in section
181
+ assert "<entml:max_thinking_length>25600</entml:max_thinking_length>" in section
182
+
183
+
178
184
  def test_thinking_prompt_on() -> None:
179
185
  section = build_entml_thinking_section({"thinking_mode": "on"})
180
186
  assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
@@ -223,6 +229,33 @@ def test_inject_no_tools_with_thinking_on() -> None:
223
229
  assert "<entml:thinking_mode>on</entml:thinking_mode>" in result
224
230
  assert "<thinking_behavior>" in result
225
231
  assert "Never skip the thinking block" in result
232
+ # thinking 块必须在 current_user_message 之后
233
+ assert result.index("</current_user_message>") < result.index("<entml:thinking_mode>")
234
+
235
+
236
+ def test_render_prompt_thinking_after_current_user() -> None:
237
+ proto = get_protocol("entml")
238
+ prompt = proto.render_prompt(
239
+ tool_descs=proto.format_tool_descs([
240
+ {
241
+ "type": "function",
242
+ "function": {
243
+ "name": "search",
244
+ "description": "Search",
245
+ "parameters": {"type": "object", "properties": {}},
246
+ },
247
+ }
248
+ ]),
249
+ lang="en",
250
+ history_text="<user>\nold\n</user>",
251
+ current_user_message="new",
252
+ protocol_options={"thinking_mode": "on", "max_thinking_length": 1000},
253
+ )
254
+ assert prompt.index("In this environment") < prompt.index("<entml:conversation_history>")
255
+ assert prompt.index("<entml:conversation_history>") < prompt.index("<current_user_message>")
256
+ assert prompt.index("</current_user_message>") < prompt.index("<entml:thinking_mode>")
257
+ assert prompt.index("<entml:thinking_mode>") < prompt.index("<thinking_behavior>")
258
+ assert prompt.rstrip().endswith("</thinking_behavior>")
226
259
 
227
260
 
228
261
  def test_inject_with_history_entml_tags() -> None:
@@ -316,7 +349,7 @@ def test_entml_history_tool_invoke_reminder_when_tools_in_history() -> None:
316
349
 
317
350
 
318
351
  def test_strip_entml_from_user_content() -> None:
319
- from echotools.exec.fncall.protocols.entml_sanitize import strip_entml_from_content
352
+ from echotools.exec.fncall.protocols.entml import strip_entml_from_content
320
353
 
321
354
  raw = (
322
355
  "请查天气\n"
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from echotools.exec.fncall.protocols.entml_thinking_history import (
3
+ from echotools.exec.fncall.protocols.entml_think.hist import (
4
4
  apply_thinking_history_policy,
5
5
  extract_reasoning_text,
6
6
  parse_interleaved_history,