echotools 2.3.60__tar.gz → 2.3.62__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.
- {echotools-2.3.60 → echotools-2.3.62}/PKG-INFO +1 -1
- {echotools-2.3.60 → echotools-2.3.62}/pyproject.toml +1 -1
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/stream.py +18 -6
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +2 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/core.py +10 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/parse.py +221 -40
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_thinking_parse.py +59 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_fncall_stream_thinking.py +65 -0
- {echotools-2.3.60 → echotools-2.3.62}/README.md +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/docs/README.md +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/setup.cfg +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/compat.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools/version.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/SOURCES.txt +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/bench_import.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/conftest.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_completion.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_config.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_console.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_errors.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_events.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_facade.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_ids.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_imports.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_io.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_keys.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_logger.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_merge.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_stream_partial_json_parity.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_usage.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_web.py +0 -0
- {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_web_broker.py +0 -0
|
@@ -12,12 +12,12 @@ from echotools.exec.protocol.base import ToolProtocol
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
# 懒导入:仅 entml 协议需要
|
|
15
|
-
def _make_thinking_filter(protocol: ToolProtocol):
|
|
15
|
+
def _make_thinking_filter(protocol: ToolProtocol, *, thinking_enabled: bool = True):
|
|
16
16
|
if getattr(protocol, "id", None) == "entml":
|
|
17
17
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
18
18
|
EntmlThinkingStreamFilter,
|
|
19
19
|
)
|
|
20
|
-
return EntmlThinkingStreamFilter()
|
|
20
|
+
return EntmlThinkingStreamFilter(thinking_enabled=thinking_enabled)
|
|
21
21
|
return None
|
|
22
22
|
|
|
23
23
|
|
|
@@ -40,9 +40,13 @@ class FncallStreamParser:
|
|
|
40
40
|
self,
|
|
41
41
|
protocol: ToolProtocol,
|
|
42
42
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
43
|
+
protocol_options: Optional[Dict[str, Any]] = None,
|
|
43
44
|
) -> None:
|
|
44
45
|
self._protocol = protocol
|
|
45
46
|
self._tools = tools
|
|
47
|
+
from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
|
|
48
|
+
|
|
49
|
+
self._thinking_enabled = is_thinking_enabled(protocol_options)
|
|
46
50
|
self._raw_buf: str = ""
|
|
47
51
|
self._text_parts: List[str] = []
|
|
48
52
|
self._waiting_tail: str = ""
|
|
@@ -51,7 +55,9 @@ class FncallStreamParser:
|
|
|
51
55
|
self._state: str = self.WAITING_FOR_TAG
|
|
52
56
|
self._finalized_result: Optional[Tuple[str, List[Dict[str, Any]]]] = None
|
|
53
57
|
self._thinking_parts: List[str] = []
|
|
54
|
-
self._thinking_filter = _make_thinking_filter(
|
|
58
|
+
self._thinking_filter = _make_thinking_filter(
|
|
59
|
+
protocol, thinking_enabled=self._thinking_enabled
|
|
60
|
+
)
|
|
55
61
|
self._emitted_invoke_count: int = 0
|
|
56
62
|
self._json_stream_encoder = None
|
|
57
63
|
self._pending_stream_deltas: Deque[Tuple[str, str]] = deque()
|
|
@@ -298,7 +304,9 @@ class FncallStreamParser:
|
|
|
298
304
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
299
305
|
invoke_index_inside_unclosed_thinking,
|
|
300
306
|
)
|
|
301
|
-
if invoke_index_inside_unclosed_thinking(
|
|
307
|
+
if invoke_index_inside_unclosed_thinking(
|
|
308
|
+
combined, pos, thinking_enabled=self._thinking_enabled
|
|
309
|
+
):
|
|
302
310
|
self._feed_waiting_thinking_plain(combined)
|
|
303
311
|
return
|
|
304
312
|
if pos > 0:
|
|
@@ -314,7 +322,9 @@ class FncallStreamParser:
|
|
|
314
322
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
315
323
|
has_unclosed_entml_thinking,
|
|
316
324
|
)
|
|
317
|
-
if has_unclosed_entml_thinking(
|
|
325
|
+
if has_unclosed_entml_thinking(
|
|
326
|
+
combined, thinking_enabled=self._thinking_enabled
|
|
327
|
+
):
|
|
318
328
|
self._feed_waiting_thinking_plain(combined)
|
|
319
329
|
return
|
|
320
330
|
|
|
@@ -633,7 +643,9 @@ class FncallStreamParser:
|
|
|
633
643
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
634
644
|
split_entml_thinking,
|
|
635
645
|
)
|
|
636
|
-
clean_text, more_thinking = split_entml_thinking(
|
|
646
|
+
clean_text, more_thinking = split_entml_thinking(
|
|
647
|
+
clean_text, thinking_enabled=self._thinking_enabled
|
|
648
|
+
)
|
|
637
649
|
if more_thinking:
|
|
638
650
|
self._thinking_parts.append(more_thinking)
|
|
639
651
|
|
{echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/__init__.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from echotools.exec.fncall.protocols.entml_think.core import (
|
|
4
4
|
build_entml_thinking_section,
|
|
5
|
+
is_thinking_enabled,
|
|
5
6
|
parse_max_thinking_length,
|
|
6
7
|
)
|
|
7
8
|
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
@@ -24,6 +25,7 @@ __all__ = [
|
|
|
24
25
|
"extract_reasoning_text",
|
|
25
26
|
"format_entml_thinking_history_block",
|
|
26
27
|
"has_unclosed_entml_thinking",
|
|
28
|
+
"is_thinking_enabled",
|
|
27
29
|
"parse_include_thinking_in_history",
|
|
28
30
|
"parse_interleaved_history",
|
|
29
31
|
"parse_max_thinking_length",
|
{echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/core.py
RENAMED
|
@@ -159,6 +159,16 @@ def resolve_thinking_injection(
|
|
|
159
159
|
return injection_mode, max_length
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def is_thinking_enabled(protocol_options: Optional[Dict[str, Any]] = None) -> bool:
|
|
163
|
+
"""是否开启思考模式(容错与 plain ``<thinking>`` 仅在此为 True 时生效)。
|
|
164
|
+
|
|
165
|
+
未传 ``protocol_options`` 时视为已开启(兼容旧调用);显式 ``off`` / ``none`` 时关闭。
|
|
166
|
+
"""
|
|
167
|
+
if protocol_options is None:
|
|
168
|
+
return True
|
|
169
|
+
return resolve_thinking_injection(protocol_options) is not None
|
|
170
|
+
|
|
171
|
+
|
|
162
172
|
def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
|
|
163
173
|
return injection_mode != "auto"
|
|
164
174
|
|
{echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -16,6 +16,7 @@ THINKING_BLOCK_RE = re.compile(
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
_THINKING_OPEN_PREFIX = "<entml:thinking"
|
|
19
|
+
_PLAIN_THINKING_OPEN_PREFIX = "<thinking"
|
|
19
20
|
_THINKING_CLOSE = "</entml:thinking>"
|
|
20
21
|
_FAULT_THINKING_CLOSE = "</thinking>"
|
|
21
22
|
# 与 thinking 共享 `<entml:` 前缀的其它标签:歧义 holdback 应交由工具流式状态机处理
|
|
@@ -116,6 +117,95 @@ def find_ambiguous_entml_tool_prefix(buffer: str) -> int:
|
|
|
116
117
|
return earliest
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
def _find_plain_thinking_open(text: str, start: int = 0) -> int:
|
|
121
|
+
"""返回 plain ``<thinking>`` 开标签起始下标(``<entml:thinking>`` 内不含此子串)。"""
|
|
122
|
+
return text.find(_PLAIN_THINKING_OPEN_PREFIX, start)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _find_earliest_thinking_open(
|
|
126
|
+
text: str,
|
|
127
|
+
start: int = 0,
|
|
128
|
+
*,
|
|
129
|
+
thinking_enabled: bool = True,
|
|
130
|
+
) -> Tuple[int, bool]:
|
|
131
|
+
"""返回 ``(开标签下标, 是否 plain)``;无则 ``(-1, False)``。"""
|
|
132
|
+
entml_at = text.find(_THINKING_OPEN_PREFIX, start)
|
|
133
|
+
if not thinking_enabled:
|
|
134
|
+
if entml_at < 0:
|
|
135
|
+
return -1, False
|
|
136
|
+
return entml_at, False
|
|
137
|
+
plain_at = _find_plain_thinking_open(text, start)
|
|
138
|
+
if entml_at < 0 and plain_at < 0:
|
|
139
|
+
return -1, False
|
|
140
|
+
if entml_at < 0:
|
|
141
|
+
return plain_at, True
|
|
142
|
+
if plain_at < 0:
|
|
143
|
+
return entml_at, False
|
|
144
|
+
if plain_at < entml_at:
|
|
145
|
+
return plain_at, True
|
|
146
|
+
return entml_at, False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _find_thinking_close(
|
|
150
|
+
text: str,
|
|
151
|
+
body_start: int,
|
|
152
|
+
*,
|
|
153
|
+
opened_plain: bool,
|
|
154
|
+
thinking_enabled: bool = True,
|
|
155
|
+
) -> Tuple[int, int]:
|
|
156
|
+
"""返回 ``(闭标签下标, 闭标签长度)``;未找到则 ``(-1, 0)``。"""
|
|
157
|
+
if not thinking_enabled:
|
|
158
|
+
close_at = text.find(_THINKING_CLOSE, body_start)
|
|
159
|
+
if close_at >= 0:
|
|
160
|
+
return close_at, len(_THINKING_CLOSE)
|
|
161
|
+
return -1, 0
|
|
162
|
+
|
|
163
|
+
if opened_plain:
|
|
164
|
+
entml_close = text.find(_THINKING_CLOSE, body_start)
|
|
165
|
+
fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
|
|
166
|
+
candidates: List[Tuple[int, int]] = []
|
|
167
|
+
if entml_close >= 0:
|
|
168
|
+
candidates.append((entml_close, len(_THINKING_CLOSE)))
|
|
169
|
+
if fault_close >= 0:
|
|
170
|
+
candidates.append((fault_close, len(_FAULT_THINKING_CLOSE)))
|
|
171
|
+
if not candidates:
|
|
172
|
+
return -1, 0
|
|
173
|
+
close_at, close_len = min(candidates, key=lambda item: item[0])
|
|
174
|
+
return close_at, close_len
|
|
175
|
+
|
|
176
|
+
entml_close = text.find(_THINKING_CLOSE, body_start)
|
|
177
|
+
fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
|
|
178
|
+
if entml_close >= 0 and (fault_close < 0 or entml_close <= fault_close):
|
|
179
|
+
return entml_close, len(_THINKING_CLOSE)
|
|
180
|
+
if fault_close < 0:
|
|
181
|
+
return -1, 0
|
|
182
|
+
|
|
183
|
+
after_fault = fault_close + len(_FAULT_THINKING_CLOSE)
|
|
184
|
+
invoke_rel = find_complete_entml_invoke_open(text[after_fault:])
|
|
185
|
+
entml_after = text.find(_THINKING_CLOSE, after_fault)
|
|
186
|
+
if invoke_rel >= 0:
|
|
187
|
+
invoke_abs = after_fault + invoke_rel
|
|
188
|
+
if entml_after < 0 or invoke_abs < entml_after:
|
|
189
|
+
return fault_close, len(_FAULT_THINKING_CLOSE)
|
|
190
|
+
if entml_after >= 0:
|
|
191
|
+
return entml_after, len(_THINKING_CLOSE)
|
|
192
|
+
return -1, 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _hold_thinking_open_prefixes(
|
|
196
|
+
buffer: str,
|
|
197
|
+
*,
|
|
198
|
+
thinking_enabled: bool = True,
|
|
199
|
+
) -> Tuple[str, str]:
|
|
200
|
+
"""hold ``<entml:thinking`` 或 plain ``<thinking`` 的真前缀。"""
|
|
201
|
+
safe, hold = _hold_prefix(buffer, _THINKING_OPEN_PREFIX)
|
|
202
|
+
if hold:
|
|
203
|
+
return safe, hold
|
|
204
|
+
if thinking_enabled:
|
|
205
|
+
return _hold_prefix(safe, _PLAIN_THINKING_OPEN_PREFIX)
|
|
206
|
+
return safe, ""
|
|
207
|
+
|
|
208
|
+
|
|
119
209
|
def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
|
|
120
210
|
"""thinking 块内:hold 可能长成真实工具调用的 markup(非 prose ``<entml:invoke>`` 提及)。"""
|
|
121
211
|
if not buffer:
|
|
@@ -143,68 +233,125 @@ def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
|
|
|
143
233
|
return buffer, ""
|
|
144
234
|
|
|
145
235
|
|
|
146
|
-
def has_unclosed_entml_thinking(
|
|
147
|
-
|
|
236
|
+
def has_unclosed_entml_thinking(
|
|
237
|
+
text: str,
|
|
238
|
+
*,
|
|
239
|
+
thinking_enabled: bool = True,
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""buffer 中是否存在尚未闭合的 thinking 块(含开标签未收齐)。"""
|
|
148
242
|
if not text:
|
|
149
243
|
return False
|
|
150
244
|
i = 0
|
|
151
245
|
while i < len(text):
|
|
152
|
-
open_at =
|
|
246
|
+
open_at, opened_plain = _find_earliest_thinking_open(
|
|
247
|
+
text, i, thinking_enabled=thinking_enabled
|
|
248
|
+
)
|
|
153
249
|
if open_at < 0:
|
|
154
250
|
break
|
|
155
251
|
gt = text.find(">", open_at)
|
|
156
252
|
if gt < 0:
|
|
157
253
|
return True
|
|
158
|
-
|
|
254
|
+
body_start = gt + 1
|
|
255
|
+
close_at, close_len = _find_thinking_close(
|
|
256
|
+
text,
|
|
257
|
+
body_start,
|
|
258
|
+
opened_plain=opened_plain,
|
|
259
|
+
thinking_enabled=thinking_enabled,
|
|
260
|
+
)
|
|
159
261
|
if close_at < 0:
|
|
160
262
|
return True
|
|
161
|
-
i = close_at +
|
|
263
|
+
i = close_at + close_len
|
|
162
264
|
max_keep = len(_THINKING_OPEN_PREFIX) - 1
|
|
265
|
+
if thinking_enabled:
|
|
266
|
+
max_keep = max(max_keep, len(_PLAIN_THINKING_OPEN_PREFIX) - 1)
|
|
163
267
|
check_len = min(len(text), max_keep)
|
|
164
268
|
for length in range(check_len, 0, -1):
|
|
165
269
|
suffix = text[-length:]
|
|
166
|
-
if
|
|
167
|
-
_THINKING_OPEN_PREFIX.startswith(suffix)
|
|
270
|
+
if (
|
|
271
|
+
_THINKING_OPEN_PREFIX.startswith(suffix)
|
|
272
|
+
and suffix != _THINKING_OPEN_PREFIX
|
|
168
273
|
):
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
274
|
+
if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
|
|
275
|
+
continue
|
|
276
|
+
return True
|
|
277
|
+
if thinking_enabled and (
|
|
278
|
+
_PLAIN_THINKING_OPEN_PREFIX.startswith(suffix)
|
|
279
|
+
and suffix != _PLAIN_THINKING_OPEN_PREFIX
|
|
280
|
+
):
|
|
281
|
+
return True
|
|
174
282
|
return False
|
|
175
283
|
|
|
176
284
|
|
|
177
|
-
def invoke_index_inside_unclosed_thinking(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
285
|
+
def invoke_index_inside_unclosed_thinking(
|
|
286
|
+
text: str,
|
|
287
|
+
invoke_at: int,
|
|
288
|
+
*,
|
|
289
|
+
thinking_enabled: bool = True,
|
|
290
|
+
) -> bool:
|
|
291
|
+
"""``invoke_at`` 是否落在尚未闭合的 thinking 块内。"""
|
|
182
292
|
if invoke_at < 0:
|
|
183
293
|
return False
|
|
184
|
-
think_open =
|
|
294
|
+
think_open, opened_plain = _find_earliest_thinking_open(
|
|
295
|
+
text, thinking_enabled=thinking_enabled
|
|
296
|
+
)
|
|
185
297
|
if think_open < 0 or invoke_at <= think_open:
|
|
186
298
|
return False
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
299
|
+
gt = text.find(">", think_open)
|
|
300
|
+
if gt < 0:
|
|
301
|
+
return True
|
|
302
|
+
body_start = gt + 1
|
|
303
|
+
close_at, close_len = _find_thinking_close(
|
|
304
|
+
text,
|
|
305
|
+
body_start,
|
|
306
|
+
opened_plain=opened_plain,
|
|
307
|
+
thinking_enabled=thinking_enabled,
|
|
308
|
+
)
|
|
309
|
+
if close_at < 0:
|
|
310
|
+
return True
|
|
311
|
+
return invoke_at < close_at + close_len
|
|
194
312
|
|
|
195
313
|
|
|
196
|
-
def split_entml_thinking(
|
|
197
|
-
|
|
314
|
+
def split_entml_thinking(
|
|
315
|
+
text: str,
|
|
316
|
+
*,
|
|
317
|
+
thinking_enabled: bool = True,
|
|
318
|
+
) -> Tuple[str, str]:
|
|
319
|
+
"""从文本中剥离 thinking 块,返回 (正文, 思考链拼接)。"""
|
|
198
320
|
if not text:
|
|
199
321
|
return "", ""
|
|
200
322
|
|
|
201
323
|
parts: List[str] = []
|
|
324
|
+
clean_parts: List[str] = []
|
|
325
|
+
i = 0
|
|
326
|
+
while i < len(text):
|
|
327
|
+
open_at, opened_plain = _find_earliest_thinking_open(
|
|
328
|
+
text, i, thinking_enabled=thinking_enabled
|
|
329
|
+
)
|
|
330
|
+
if open_at < 0:
|
|
331
|
+
clean_parts.append(text[i:])
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
clean_parts.append(text[i:open_at])
|
|
335
|
+
gt = text.find(">", open_at)
|
|
336
|
+
if gt < 0:
|
|
337
|
+
clean_parts.append(text[open_at:])
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
body_start = gt + 1
|
|
341
|
+
close_at, close_len = _find_thinking_close(
|
|
342
|
+
text,
|
|
343
|
+
body_start,
|
|
344
|
+
opened_plain=opened_plain,
|
|
345
|
+
thinking_enabled=thinking_enabled,
|
|
346
|
+
)
|
|
347
|
+
if close_at < 0:
|
|
348
|
+
clean_parts.append(text[open_at:])
|
|
349
|
+
break
|
|
202
350
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return ""
|
|
351
|
+
parts.append(text[body_start:close_at])
|
|
352
|
+
i = close_at + close_len
|
|
206
353
|
|
|
207
|
-
clean =
|
|
354
|
+
clean = "".join(clean_parts)
|
|
208
355
|
thinking = "\n".join(part.strip() for part in parts if part.strip())
|
|
209
356
|
return strip_orphan_thinking_close_prefix(clean.strip()), thinking
|
|
210
357
|
|
|
@@ -241,9 +388,11 @@ class EntmlThinkingStreamFilter:
|
|
|
241
388
|
块内正文在收到时即增量输出 thinking,无需等闭合标签。
|
|
242
389
|
"""
|
|
243
390
|
|
|
244
|
-
def __init__(self) -> None:
|
|
391
|
+
def __init__(self, *, thinking_enabled: bool = True) -> None:
|
|
392
|
+
self._thinking_enabled = thinking_enabled
|
|
245
393
|
self._pending = ""
|
|
246
394
|
self._in_block = False
|
|
395
|
+
self._opened_plain = False
|
|
247
396
|
# 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
|
|
248
397
|
self._thinking_started = False
|
|
249
398
|
# 见到 ``</thinking>`` 后暂存,仅当后续出现 invoke 才视为闭合;否则当纯文本
|
|
@@ -253,20 +402,25 @@ class EntmlThinkingStreamFilter:
|
|
|
253
402
|
def clear_invoke_handoff_state(self) -> None:
|
|
254
403
|
"""invoke 已交给正文/工具解析后,结束 thinking / fault 观察状态。"""
|
|
255
404
|
self._in_block = False
|
|
405
|
+
self._opened_plain = False
|
|
256
406
|
self._fault_watch = False
|
|
257
407
|
self._fault_buffer = ""
|
|
258
408
|
self._thinking_started = False
|
|
259
409
|
|
|
260
410
|
def in_open_thinking(self) -> bool:
|
|
261
|
-
"""
|
|
411
|
+
"""思考块已开始且尚未闭合(含开/闭标签分片 hold)。"""
|
|
262
412
|
if self._in_block or self._fault_watch:
|
|
263
413
|
return True
|
|
264
|
-
open_at =
|
|
414
|
+
open_at, _ = _find_earliest_thinking_open(
|
|
415
|
+
self._pending, thinking_enabled=self._thinking_enabled
|
|
416
|
+
)
|
|
265
417
|
if open_at >= 0:
|
|
266
418
|
gt = self._pending.find(">", open_at)
|
|
267
419
|
if gt < 0:
|
|
268
420
|
return True
|
|
269
|
-
_, hold =
|
|
421
|
+
_, hold = _hold_thinking_open_prefixes(
|
|
422
|
+
self._pending, thinking_enabled=self._thinking_enabled
|
|
423
|
+
)
|
|
270
424
|
if hold:
|
|
271
425
|
return True
|
|
272
426
|
return False
|
|
@@ -279,6 +433,7 @@ class EntmlThinkingStreamFilter:
|
|
|
279
433
|
if invoke_at < len(_FAULT_THINKING_CLOSE):
|
|
280
434
|
return False
|
|
281
435
|
self._in_block = False
|
|
436
|
+
self._opened_plain = False
|
|
282
437
|
self._fault_watch = False
|
|
283
438
|
self._thinking_started = False
|
|
284
439
|
self._pending = self._fault_buffer[len(_FAULT_THINKING_CLOSE) :]
|
|
@@ -296,6 +451,7 @@ class EntmlThinkingStreamFilter:
|
|
|
296
451
|
if emitted:
|
|
297
452
|
out.append(("thinking", emitted))
|
|
298
453
|
self._in_block = False
|
|
454
|
+
self._opened_plain = False
|
|
299
455
|
self._fault_watch = False
|
|
300
456
|
self._thinking_started = False
|
|
301
457
|
self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
|
|
@@ -323,12 +479,15 @@ class EntmlThinkingStreamFilter:
|
|
|
323
479
|
out.append(("thinking", emitted))
|
|
324
480
|
out.append(("content", invoke_block))
|
|
325
481
|
self._in_block = False
|
|
482
|
+
self._opened_plain = False
|
|
326
483
|
self._fault_watch = False
|
|
327
484
|
self._fault_buffer = ""
|
|
328
485
|
self._thinking_started = False
|
|
329
486
|
rest = after.lstrip()
|
|
330
487
|
if rest.startswith(_THINKING_CLOSE):
|
|
331
488
|
rest = rest[len(_THINKING_CLOSE) :].lstrip()
|
|
489
|
+
if rest.startswith(_FAULT_THINKING_CLOSE):
|
|
490
|
+
rest = rest[len(_FAULT_THINKING_CLOSE) :].lstrip()
|
|
332
491
|
self._pending = rest
|
|
333
492
|
return True
|
|
334
493
|
return False
|
|
@@ -361,6 +520,7 @@ class EntmlThinkingStreamFilter:
|
|
|
361
520
|
if emitted:
|
|
362
521
|
out.append(("thinking", emitted))
|
|
363
522
|
self._in_block = False
|
|
523
|
+
self._opened_plain = False
|
|
364
524
|
self._fault_watch = False
|
|
365
525
|
self._fault_buffer = ""
|
|
366
526
|
self._thinking_started = False
|
|
@@ -371,8 +531,19 @@ class EntmlThinkingStreamFilter:
|
|
|
371
531
|
return True
|
|
372
532
|
|
|
373
533
|
fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
|
|
374
|
-
if fault_at >= 0:
|
|
534
|
+
if fault_at >= 0 and self._thinking_enabled:
|
|
375
535
|
before = self._pending[:fault_at]
|
|
536
|
+
if self._opened_plain:
|
|
537
|
+
emitted = self._emit_thinking_piece(before)
|
|
538
|
+
if emitted:
|
|
539
|
+
out.append(("thinking", emitted))
|
|
540
|
+
self._in_block = False
|
|
541
|
+
self._opened_plain = False
|
|
542
|
+
self._fault_watch = False
|
|
543
|
+
self._fault_buffer = ""
|
|
544
|
+
self._thinking_started = False
|
|
545
|
+
self._pending = self._pending[fault_at + len(_FAULT_THINKING_CLOSE) :]
|
|
546
|
+
return True
|
|
376
547
|
emitted = self._emit_thinking_piece(before)
|
|
377
548
|
if emitted:
|
|
378
549
|
out.append(("thinking", emitted))
|
|
@@ -391,7 +562,7 @@ class EntmlThinkingStreamFilter:
|
|
|
391
562
|
return False
|
|
392
563
|
|
|
393
564
|
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
394
|
-
if not hold:
|
|
565
|
+
if not hold and self._thinking_enabled:
|
|
395
566
|
safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
|
|
396
567
|
if safe:
|
|
397
568
|
emitted = self._emit_thinking_piece(safe)
|
|
@@ -402,9 +573,13 @@ class EntmlThinkingStreamFilter:
|
|
|
402
573
|
|
|
403
574
|
def _feed_before_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
404
575
|
"""开标签之前或尚未进入块;返回 False 表示应退出 feed 主循环。"""
|
|
405
|
-
open_at =
|
|
576
|
+
open_at, opened_plain = _find_earliest_thinking_open(
|
|
577
|
+
self._pending, thinking_enabled=self._thinking_enabled
|
|
578
|
+
)
|
|
406
579
|
if open_at < 0:
|
|
407
|
-
safe, self._pending =
|
|
580
|
+
safe, self._pending = _hold_thinking_open_prefixes(
|
|
581
|
+
self._pending, thinking_enabled=self._thinking_enabled
|
|
582
|
+
)
|
|
408
583
|
if safe:
|
|
409
584
|
out.append(("content", safe))
|
|
410
585
|
return False
|
|
@@ -418,6 +593,7 @@ class EntmlThinkingStreamFilter:
|
|
|
418
593
|
return False
|
|
419
594
|
|
|
420
595
|
self._in_block = True
|
|
596
|
+
self._opened_plain = opened_plain
|
|
421
597
|
self._thinking_started = False
|
|
422
598
|
self._pending = self._pending[gt + 1 :]
|
|
423
599
|
return True
|
|
@@ -471,6 +647,7 @@ class EntmlThinkingStreamFilter:
|
|
|
471
647
|
self._fault_buffer = ""
|
|
472
648
|
self._fault_watch = False
|
|
473
649
|
self._in_block = False
|
|
650
|
+
self._opened_plain = False
|
|
474
651
|
self._thinking_started = False
|
|
475
652
|
if self._in_block:
|
|
476
653
|
if not self._try_emit_actionable_invoke_from_in_block(out):
|
|
@@ -479,9 +656,12 @@ class EntmlThinkingStreamFilter:
|
|
|
479
656
|
out.append(("thinking", emitted))
|
|
480
657
|
self._pending = ""
|
|
481
658
|
self._in_block = False
|
|
659
|
+
self._opened_plain = False
|
|
482
660
|
self._thinking_started = False
|
|
483
661
|
if self._pending:
|
|
484
|
-
content, thinking = split_entml_thinking(
|
|
662
|
+
content, thinking = split_entml_thinking(
|
|
663
|
+
self._pending, thinking_enabled=self._thinking_enabled
|
|
664
|
+
)
|
|
485
665
|
if thinking:
|
|
486
666
|
out.append(("thinking", thinking))
|
|
487
667
|
if content:
|
|
@@ -490,6 +670,7 @@ class EntmlThinkingStreamFilter:
|
|
|
490
670
|
out.append(("content", self._pending))
|
|
491
671
|
self._pending = ""
|
|
492
672
|
self._in_block = False
|
|
673
|
+
self._opened_plain = False
|
|
493
674
|
self._fault_watch = False
|
|
494
675
|
self._fault_buffer = ""
|
|
495
676
|
self._thinking_started = False
|
|
@@ -92,3 +92,62 @@ def test_entml_thinking_hold_close_prefix() -> None:
|
|
|
92
92
|
content = "".join(t for k, t in parts if k == "content")
|
|
93
93
|
assert thinking == "ab"
|
|
94
94
|
assert content == "cd"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_plain_thinking_open_entml_close_batch() -> None:
|
|
98
|
+
text = "<thinking>\nplan step\n</entml:thinking>\nanswer"
|
|
99
|
+
content, thinking = split_entml_thinking(text)
|
|
100
|
+
assert thinking == "plan step"
|
|
101
|
+
assert "answer" in content
|
|
102
|
+
assert "<thinking>" not in content
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_plain_thinking_open_fault_close_batch() -> None:
|
|
106
|
+
text = "<thinking>\nplan step\n</thinking>\nanswer"
|
|
107
|
+
content, thinking = split_entml_thinking(text)
|
|
108
|
+
assert thinking == "plan step"
|
|
109
|
+
assert "answer" in content
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_plain_thinking_stream_entml_close() -> None:
|
|
113
|
+
filt = EntmlThinkingStreamFilter()
|
|
114
|
+
parts = []
|
|
115
|
+
for chunk in ["<thinking>\nplan", "\n</entml:thinking>\n", "hello"]:
|
|
116
|
+
parts.extend(filt.feed(chunk))
|
|
117
|
+
parts.extend(filt.finalize())
|
|
118
|
+
thinking = "".join(t for k, t in parts if k == "thinking")
|
|
119
|
+
content = "".join(t for k, t in parts if k == "content")
|
|
120
|
+
assert thinking.strip() == "plan"
|
|
121
|
+
assert "hello" in content
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_plain_thinking_stream_fault_close() -> None:
|
|
125
|
+
filt = EntmlThinkingStreamFilter()
|
|
126
|
+
parts = []
|
|
127
|
+
for chunk in ["<thinking>\nplan\n</thinking>\n", "hello"]:
|
|
128
|
+
parts.extend(filt.feed(chunk))
|
|
129
|
+
parts.extend(filt.finalize())
|
|
130
|
+
thinking = "".join(t for k, t in parts if k == "thinking")
|
|
131
|
+
content = "".join(t for k, t in parts if k == "content")
|
|
132
|
+
assert thinking.strip() == "plan"
|
|
133
|
+
assert "hello" in content
|
|
134
|
+
assert not filt.in_open_thinking()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_is_thinking_enabled() -> None:
|
|
138
|
+
from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
|
|
139
|
+
|
|
140
|
+
assert is_thinking_enabled(None)
|
|
141
|
+
assert is_thinking_enabled({"thinking_mode": "on"})
|
|
142
|
+
assert is_thinking_enabled({"thinking_mode": "auto"})
|
|
143
|
+
assert not is_thinking_enabled({"thinking_mode": "off"})
|
|
144
|
+
assert not is_thinking_enabled({"thinking_level": "none"})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_plain_thinking_requires_thinking_enabled_batch() -> None:
|
|
148
|
+
text = "<thinking>\nplan\n</thinking>\nanswer"
|
|
149
|
+
_, thinking_on = split_entml_thinking(text, thinking_enabled=True)
|
|
150
|
+
content_off, thinking_off = split_entml_thinking(text, thinking_enabled=False)
|
|
151
|
+
assert thinking_on == "plan"
|
|
152
|
+
assert thinking_off == ""
|
|
153
|
+
assert "<thinking>" in content_off
|