echotools 2.3.49__tar.gz → 2.3.51__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {echotools-2.3.49 → echotools-2.3.51}/PKG-INFO +1 -1
- {echotools-2.3.49 → echotools-2.3.51}/pyproject.toml +1 -1
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/stream.py +139 -22
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history.py +0 -19
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history_format.py +2 -5
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -18
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/core.py +1 -1
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/parse.py +141 -9
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/SOURCES.txt +2 -0
- echotools-2.3.51/src/tests/test_entml_agent_output_formats.py +143 -0
- echotools-2.3.51/src/tests/test_entml_parser_comprehensive.py +656 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_fncall_stream_thinking.py +47 -12
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_stream_realtime_parse.py +3 -3
- {echotools-2.3.49 → echotools-2.3.51}/README.md +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/docs/README.md +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/setup.cfg +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/compat.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools/version.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/bench_import.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/conftest.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_completion.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_config.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_console.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_errors.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_events.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_facade.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_ids.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_imports.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_io.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_keys.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_logger.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_merge.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_usage.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_web.py +0 -0
- {echotools-2.3.49 → echotools-2.3.51}/src/tests/test_web_broker.py +0 -0
|
@@ -139,8 +139,25 @@ class FncallStreamParser:
|
|
|
139
139
|
return buffer, ""
|
|
140
140
|
|
|
141
141
|
def _append_content_text(self, text: str) -> None:
|
|
142
|
-
if text:
|
|
143
|
-
|
|
142
|
+
if not text:
|
|
143
|
+
return
|
|
144
|
+
from echotools.exec.fncall.protocols.entml_think.parse import _THINKING_CLOSE
|
|
145
|
+
|
|
146
|
+
remainder = text
|
|
147
|
+
while True:
|
|
148
|
+
stripped = remainder.strip()
|
|
149
|
+
if not stripped:
|
|
150
|
+
return
|
|
151
|
+
if stripped == _THINKING_CLOSE:
|
|
152
|
+
return
|
|
153
|
+
if _THINKING_CLOSE in remainder:
|
|
154
|
+
before, _, after = remainder.partition(_THINKING_CLOSE)
|
|
155
|
+
if before.strip():
|
|
156
|
+
self._text_parts.append(before)
|
|
157
|
+
remainder = after
|
|
158
|
+
continue
|
|
159
|
+
self._text_parts.append(remainder)
|
|
160
|
+
return
|
|
144
161
|
|
|
145
162
|
def _normalize_stream_chunk(self, text: str) -> str:
|
|
146
163
|
fn = getattr(self._protocol, "normalize_stream_buffer", None)
|
|
@@ -148,11 +165,64 @@ class FncallStreamParser:
|
|
|
148
165
|
return fn(text)
|
|
149
166
|
return text
|
|
150
167
|
|
|
168
|
+
def _invoke_close_tag(self) -> str:
|
|
169
|
+
return self._end_tags[0] if self._end_tags else "</entml:invoke>"
|
|
170
|
+
|
|
171
|
+
def _drain_thinking_holdback_to_fncall(self) -> None:
|
|
172
|
+
"""invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
|
|
173
|
+
if self._thinking_filter is None:
|
|
174
|
+
return
|
|
175
|
+
pending = self._thinking_filter._pending
|
|
176
|
+
if pending:
|
|
177
|
+
self._fncall_buf += pending
|
|
178
|
+
self._thinking_filter._pending = ""
|
|
179
|
+
|
|
180
|
+
def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
|
|
181
|
+
self._fncall_buf = buffer_from[pos:]
|
|
182
|
+
self._drain_thinking_holdback_to_fncall()
|
|
183
|
+
self._detected = True
|
|
184
|
+
self._state = self.IN_FUNCTION_CALLS
|
|
185
|
+
self._json_stream_encoder = None
|
|
186
|
+
if self._thinking_filter is not None:
|
|
187
|
+
self._thinking_filter.clear_invoke_handoff_state()
|
|
188
|
+
|
|
151
189
|
def _feed_content_waiting(self, text: str) -> None:
|
|
152
190
|
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
153
191
|
if not text:
|
|
154
192
|
return
|
|
155
193
|
text = self._normalize_stream_chunk(text)
|
|
194
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
195
|
+
close_tag = self._invoke_close_tag()
|
|
196
|
+
if not self._is_call_closed():
|
|
197
|
+
if close_tag in text:
|
|
198
|
+
idx = text.find(close_tag)
|
|
199
|
+
self._fncall_buf += text[: idx + len(close_tag)]
|
|
200
|
+
tail = text[idx + len(close_tag) :]
|
|
201
|
+
if tail:
|
|
202
|
+
self._feed_content_waiting(tail)
|
|
203
|
+
return
|
|
204
|
+
self._fncall_buf += text
|
|
205
|
+
return
|
|
206
|
+
trigger_tags = self._protocol.get_trigger_tags()
|
|
207
|
+
found, pos = self._protocol.detect_start(text)
|
|
208
|
+
if found:
|
|
209
|
+
if pos > 0:
|
|
210
|
+
self._append_content_text(text[:pos])
|
|
211
|
+
self._fncall_buf += text[pos:]
|
|
212
|
+
return
|
|
213
|
+
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
214
|
+
if hold_from_fn is not None:
|
|
215
|
+
hold_from = hold_from_fn(text)
|
|
216
|
+
if hold_from is not None:
|
|
217
|
+
if hold_from > 0:
|
|
218
|
+
self._append_content_text(text[:hold_from])
|
|
219
|
+
self._waiting_tail = text[hold_from:]
|
|
220
|
+
return
|
|
221
|
+
safe, remain = self._split_safe_text(text, trigger_tags)
|
|
222
|
+
if safe:
|
|
223
|
+
self._append_content_text(safe)
|
|
224
|
+
self._waiting_tail = remain
|
|
225
|
+
return
|
|
156
226
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
157
227
|
found, pos = self._protocol.detect_start(text)
|
|
158
228
|
if not found:
|
|
@@ -173,11 +243,7 @@ class FncallStreamParser:
|
|
|
173
243
|
if pos > 0:
|
|
174
244
|
self._append_content_text(text[:pos])
|
|
175
245
|
|
|
176
|
-
self.
|
|
177
|
-
self._detected = True
|
|
178
|
-
self._state = self.IN_FUNCTION_CALLS
|
|
179
|
-
if self._is_call_closed():
|
|
180
|
-
self._state = self.DONE
|
|
246
|
+
self._begin_function_calls(text, pos=pos)
|
|
181
247
|
|
|
182
248
|
def _feed_waiting_thinking_plain(self, combined: str) -> None:
|
|
183
249
|
"""未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
|
|
@@ -187,12 +253,28 @@ class FncallStreamParser:
|
|
|
187
253
|
self._thinking_parts.append(part)
|
|
188
254
|
elif part:
|
|
189
255
|
self._feed_content_waiting(part)
|
|
256
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
257
|
+
self._drain_thinking_holdback_to_fncall()
|
|
190
258
|
|
|
191
259
|
def _feed_waiting(self, chunk: str) -> None:
|
|
192
|
-
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
260
|
+
"""在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
|
|
193
261
|
combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
|
|
194
262
|
self._waiting_tail = ""
|
|
195
263
|
|
|
264
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
265
|
+
if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
|
|
266
|
+
self._feed_waiting_thinking_plain(combined)
|
|
267
|
+
return
|
|
268
|
+
if self._waiting_tail:
|
|
269
|
+
self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
|
|
270
|
+
self._waiting_tail = ""
|
|
271
|
+
self._drain_thinking_holdback_to_fncall()
|
|
272
|
+
if not self._is_call_closed():
|
|
273
|
+
self._fncall_buf += combined
|
|
274
|
+
return
|
|
275
|
+
self._feed_content_waiting(combined)
|
|
276
|
+
return
|
|
277
|
+
|
|
196
278
|
# 已进入 thinking 块时,块内不做 invoke 检测。
|
|
197
279
|
if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
|
|
198
280
|
self._feed_waiting_thinking_plain(combined)
|
|
@@ -202,14 +284,17 @@ class FncallStreamParser:
|
|
|
202
284
|
found, pos = self._protocol.detect_start(combined)
|
|
203
285
|
|
|
204
286
|
if found:
|
|
287
|
+
if self._thinking_filter is not None:
|
|
288
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
289
|
+
invoke_index_inside_unclosed_thinking,
|
|
290
|
+
)
|
|
291
|
+
if invoke_index_inside_unclosed_thinking(combined, pos):
|
|
292
|
+
self._feed_waiting_thinking_plain(combined)
|
|
293
|
+
return
|
|
205
294
|
if pos > 0:
|
|
206
295
|
# 工具标签前的正文仍可能含 thinking,走过滤器。
|
|
207
296
|
self._emit_text(combined[:pos])
|
|
208
|
-
self.
|
|
209
|
-
self._detected = True
|
|
210
|
-
self._state = self.IN_FUNCTION_CALLS
|
|
211
|
-
if self._is_call_closed():
|
|
212
|
-
self._state = self.DONE
|
|
297
|
+
self._begin_function_calls(combined, pos=pos)
|
|
213
298
|
return
|
|
214
299
|
|
|
215
300
|
# 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
|
|
@@ -261,25 +346,20 @@ class FncallStreamParser:
|
|
|
261
346
|
|
|
262
347
|
self._raw_buf += chunk
|
|
263
348
|
|
|
264
|
-
if self._state
|
|
349
|
+
if self._state != self.DONE:
|
|
265
350
|
self._feed_waiting(chunk)
|
|
266
|
-
else:
|
|
267
|
-
self._fncall_buf += self._normalize_stream_chunk(chunk)
|
|
268
|
-
if self._is_call_closed():
|
|
269
|
-
self._state = self.DONE
|
|
270
351
|
|
|
271
352
|
self._pending_stream_delta = self._poll_streaming_tool_input_delta()
|
|
272
353
|
ready = self.get_ready_tool_calls()
|
|
273
|
-
if ready:
|
|
274
|
-
|
|
275
|
-
self._json_stream_encoder.reset()
|
|
354
|
+
if ready and self._json_stream_encoder is not None:
|
|
355
|
+
self._json_stream_encoder.reset()
|
|
276
356
|
return ready
|
|
277
357
|
|
|
278
358
|
def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
|
|
279
359
|
"""invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
|
|
280
360
|
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
281
361
|
return None
|
|
282
|
-
if self._state not in (self.IN_FUNCTION_CALLS,
|
|
362
|
+
if self._state not in (self.IN_FUNCTION_CALLS,):
|
|
283
363
|
return None
|
|
284
364
|
from echotools.exec.fncall.protocols.entml_stream_json import (
|
|
285
365
|
EntmlInvokeJsonStreamEncoder,
|
|
@@ -304,6 +384,43 @@ class FncallStreamParser:
|
|
|
304
384
|
self._pending_stream_delta = None
|
|
305
385
|
return delta
|
|
306
386
|
|
|
387
|
+
@property
|
|
388
|
+
def streaming_invoke_closed(self) -> bool:
|
|
389
|
+
"""流式 fncall 缓冲中是否已出现 ``</entml:invoke>``。"""
|
|
390
|
+
if getattr(self._protocol, "id", None) != "entml":
|
|
391
|
+
return False
|
|
392
|
+
from echotools.exec.fncall.protocols.entml_stream_json import _INVOKE_CLOSE
|
|
393
|
+
|
|
394
|
+
return _INVOKE_CLOSE in self._fncall_buf
|
|
395
|
+
|
|
396
|
+
def complete_stream_delta_if_needed(self) -> Optional[Tuple[str, str]]:
|
|
397
|
+
"""invoke 已开标签但未闭合时,补齐 partial_json 尾部的合法 JSON 后缀。"""
|
|
398
|
+
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
399
|
+
return None
|
|
400
|
+
if self.streaming_invoke_closed:
|
|
401
|
+
return None
|
|
402
|
+
from echotools.exec.fncall.protocols.entml_stream_json import (
|
|
403
|
+
_INVOKE_CLOSE,
|
|
404
|
+
EntmlInvokeJsonStreamEncoder,
|
|
405
|
+
build_streaming_json_snapshot,
|
|
406
|
+
split_invoke_open,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
410
|
+
if not parsed:
|
|
411
|
+
return None
|
|
412
|
+
name, body_start = parsed
|
|
413
|
+
body = self._fncall_buf[body_start:]
|
|
414
|
+
snapshot = build_streaming_json_snapshot(body + _INVOKE_CLOSE)
|
|
415
|
+
if not snapshot:
|
|
416
|
+
return None
|
|
417
|
+
if self._json_stream_encoder is None:
|
|
418
|
+
self._json_stream_encoder = EntmlInvokeJsonStreamEncoder()
|
|
419
|
+
delta = self._json_stream_encoder.poll(body + _INVOKE_CLOSE)
|
|
420
|
+
if not delta:
|
|
421
|
+
return None
|
|
422
|
+
return (name, delta)
|
|
423
|
+
|
|
307
424
|
def _assembly_for_tool_parse(self) -> str:
|
|
308
425
|
"""可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
|
|
309
426
|
return "".join(self._text_parts) + self._waiting_tail + self._fncall_buf
|
|
@@ -319,22 +319,3 @@ def _normalize_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
319
319
|
return inject_orphan_tool_results(step2)
|
|
320
320
|
|
|
321
321
|
|
|
322
|
-
# ---------------------------------------------------------------------------
|
|
323
|
-
# 对话历史格式化
|
|
324
|
-
# ---------------------------------------------------------------------------
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def _make_assistant_dedup_key(
|
|
328
|
-
content: Optional[str],
|
|
329
|
-
tool_calls: List[Dict[str, Any]],
|
|
330
|
-
) -> Tuple[str, Tuple[Tuple[str, str], ...]]:
|
|
331
|
-
"""生成 assistant 消息的去重键。"""
|
|
332
|
-
safe_content = content or ""
|
|
333
|
-
tc_key: Tuple[Tuple[str, str], ...] = tuple(
|
|
334
|
-
(
|
|
335
|
-
(tc.get("function") or {}).get("name") or "",
|
|
336
|
-
(tc.get("function") or {}).get("arguments") or "",
|
|
337
|
-
)
|
|
338
|
-
for tc in tool_calls
|
|
339
|
-
)
|
|
340
|
-
return (safe_content, tc_key)
|
|
@@ -41,7 +41,6 @@ def _append_assistant_part(
|
|
|
41
41
|
index: int,
|
|
42
42
|
protocol: Optional[Any],
|
|
43
43
|
call_id_to_name: Dict[str, str],
|
|
44
|
-
seen_assistant_keys: set,
|
|
45
44
|
*,
|
|
46
45
|
include_thinking_in_history: bool,
|
|
47
46
|
) -> int:
|
|
@@ -55,7 +54,7 @@ def _append_assistant_part(
|
|
|
55
54
|
tool_msgs.append(messages[j])
|
|
56
55
|
j += 1
|
|
57
56
|
blocks = format_assistant_block_with_results(
|
|
58
|
-
m, tool_msgs, protocol, call_id_to_name,
|
|
57
|
+
m, tool_msgs, protocol, call_id_to_name,
|
|
59
58
|
include_thinking_in_history=include_thinking_in_history,
|
|
60
59
|
)
|
|
61
60
|
for block in blocks:
|
|
@@ -63,7 +62,7 @@ def _append_assistant_part(
|
|
|
63
62
|
return j
|
|
64
63
|
|
|
65
64
|
blocks = format_assistant_block(
|
|
66
|
-
m, content_str, protocol, call_id_to_name,
|
|
65
|
+
m, content_str, protocol, call_id_to_name,
|
|
67
66
|
include_thinking_in_history=include_thinking_in_history,
|
|
68
67
|
)
|
|
69
68
|
for block in blocks:
|
|
@@ -81,7 +80,6 @@ def _format_conversation_history(
|
|
|
81
80
|
return ""
|
|
82
81
|
|
|
83
82
|
call_id_to_name: Dict[str, str] = {}
|
|
84
|
-
seen_assistant_keys = set()
|
|
85
83
|
parts: List[Tuple[str, bool]] = []
|
|
86
84
|
clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
|
|
87
85
|
|
|
@@ -98,7 +96,6 @@ def _format_conversation_history(
|
|
|
98
96
|
if role == "assistant":
|
|
99
97
|
i = _append_assistant_part(
|
|
100
98
|
parts, messages, i, protocol, call_id_to_name,
|
|
101
|
-
seen_assistant_keys,
|
|
102
99
|
include_thinking_in_history=include_thinking_in_history,
|
|
103
100
|
)
|
|
104
101
|
continue
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
4
4
|
|
|
5
|
-
from echotools.base.logger.manager import get_logger
|
|
6
5
|
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
7
6
|
extract_reasoning_text,
|
|
8
7
|
format_entml_thinking_history_block,
|
|
@@ -12,15 +11,12 @@ from echotools.exec.fncall.shared.normalization import normalize_content
|
|
|
12
11
|
from .history import (
|
|
13
12
|
_TOOL_CALL_MARKER_RE,
|
|
14
13
|
_convert_assistant_pseudo_calls,
|
|
15
|
-
_make_assistant_dedup_key,
|
|
16
14
|
_render_inline_call_result,
|
|
17
15
|
_render_tool_call,
|
|
18
16
|
_render_tool_result,
|
|
19
17
|
_try_convert_user_to_tool,
|
|
20
18
|
)
|
|
21
19
|
|
|
22
|
-
logger = get_logger(__name__)
|
|
23
|
-
|
|
24
20
|
|
|
25
21
|
def _assistant_history_content_blocks(
|
|
26
22
|
m: Dict[str, Any],
|
|
@@ -139,19 +135,12 @@ def format_assistant_block(
|
|
|
139
135
|
content_str: str,
|
|
140
136
|
protocol: Optional[Any],
|
|
141
137
|
call_id_to_name: Dict[str, str],
|
|
142
|
-
seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
|
|
143
138
|
*,
|
|
144
139
|
include_thinking_in_history: bool = False,
|
|
145
140
|
) -> List[str]:
|
|
146
141
|
tcs: List[Dict[str, Any]] = m.get("tool_calls") or []
|
|
147
142
|
_register_tool_call_names(tcs, call_id_to_name)
|
|
148
143
|
|
|
149
|
-
dedup_key = _make_assistant_dedup_key(content_str, tcs)
|
|
150
|
-
if dedup_key in seen_assistant_keys:
|
|
151
|
-
logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
|
|
152
|
-
return []
|
|
153
|
-
seen_assistant_keys.add(dedup_key)
|
|
154
|
-
|
|
155
144
|
parts: List[str] = []
|
|
156
145
|
assistant = _format_assistant_text_block(
|
|
157
146
|
m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
|
|
@@ -174,7 +163,6 @@ def format_assistant_block_with_results(
|
|
|
174
163
|
tool_msgs: List[Dict[str, Any]],
|
|
175
164
|
protocol: Optional[Any],
|
|
176
165
|
call_id_to_name: Dict[str, str],
|
|
177
|
-
seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
|
|
178
166
|
*,
|
|
179
167
|
include_thinking_in_history: bool = False,
|
|
180
168
|
) -> List[str]:
|
|
@@ -182,12 +170,6 @@ def format_assistant_block_with_results(
|
|
|
182
170
|
content_str = normalize_content(m.get("content", ""))
|
|
183
171
|
_register_tool_call_names(tcs, call_id_to_name)
|
|
184
172
|
|
|
185
|
-
dedup_key = _make_assistant_dedup_key(content_str, tcs)
|
|
186
|
-
if dedup_key in seen_assistant_keys:
|
|
187
|
-
logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
|
|
188
|
-
return []
|
|
189
|
-
seen_assistant_keys.add(dedup_key)
|
|
190
|
-
|
|
191
173
|
parts: List[str] = []
|
|
192
174
|
assistant = _format_assistant_text_block(
|
|
193
175
|
m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
|
{echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/core.py
RENAMED
|
@@ -65,7 +65,7 @@ _LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
|
|
|
65
65
|
_THINKING_BEHAVIOR_ON_WITH_TOOLS = """\
|
|
66
66
|
Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
|
|
67
67
|
|
|
68
|
-
You MUST output a <entml:thinking> block before any other content in every reply. Only after that block
|
|
68
|
+
You MUST output a <entml:thinking> block before any other content in every reply. Only after that block closes with </entml:thinking>, output your visible reply and/or <entml:invoke> tool call(s). Never place <entml:invoke> inside <entml:thinking>. Never skip the thinking block."""
|
|
69
69
|
|
|
70
70
|
_THINKING_BEHAVIOR_ON_NO_TOOLS = """\
|
|
71
71
|
Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
|
{echotools-2.3.49 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from typing import List, Tuple
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
7
|
+
BLOCK_RE,
|
|
7
8
|
extract_attr_value,
|
|
8
9
|
normalize_entml_name,
|
|
9
10
|
)
|
|
@@ -26,6 +27,62 @@ _AMBIGUOUS_ENTML_PREFIXES = (
|
|
|
26
27
|
_INVOKE_PREFIX = "<entml:invoke"
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _is_placeholder_invoke_name(name: str) -> bool:
|
|
31
|
+
"""提示词占位符(如 ``$FUNCTION_NAME``)不算真实工具调用。"""
|
|
32
|
+
n = (name or "").strip()
|
|
33
|
+
return not n or "$" in n
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _invoke_name_from_block(block: str) -> Optional[str]:
|
|
37
|
+
prefix_len = len(_INVOKE_PREFIX)
|
|
38
|
+
pos = block.find(_INVOKE_PREFIX)
|
|
39
|
+
if pos < 0:
|
|
40
|
+
return None
|
|
41
|
+
gt = block.find(">", pos + prefix_len)
|
|
42
|
+
if gt < 0:
|
|
43
|
+
return None
|
|
44
|
+
attrs = block[pos + prefix_len : gt]
|
|
45
|
+
name = extract_attr_value(attrs, "name")
|
|
46
|
+
if not name:
|
|
47
|
+
return None
|
|
48
|
+
return normalize_entml_name(name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def find_actionable_invoke_block(buffer: str) -> Optional[Tuple[str, str, str]]:
|
|
52
|
+
"""返回首个非占位符的完整 invoke 块:``(before, block, after)``。"""
|
|
53
|
+
if not buffer:
|
|
54
|
+
return None
|
|
55
|
+
for match in BLOCK_RE.finditer(buffer):
|
|
56
|
+
block = match.group(0)
|
|
57
|
+
name = _invoke_name_from_block(block)
|
|
58
|
+
if not name or _is_placeholder_invoke_name(name):
|
|
59
|
+
continue
|
|
60
|
+
return buffer[: match.start()], block, buffer[match.end() :]
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_actionable_invoke_open(buffer: str) -> int:
|
|
65
|
+
"""返回首个非占位符且已闭合 ``>`` 的 invoke 开标签下标;否则 -1。"""
|
|
66
|
+
if not buffer:
|
|
67
|
+
return -1
|
|
68
|
+
search_from = 0
|
|
69
|
+
prefix_len = len(_INVOKE_PREFIX)
|
|
70
|
+
while True:
|
|
71
|
+
pos = buffer.find(_INVOKE_PREFIX, search_from)
|
|
72
|
+
if pos < 0:
|
|
73
|
+
return -1
|
|
74
|
+
close = buffer.find(">", pos + prefix_len)
|
|
75
|
+
if close < 0:
|
|
76
|
+
return -1
|
|
77
|
+
attrs = buffer[pos + prefix_len : close]
|
|
78
|
+
name = extract_attr_value(attrs, "name")
|
|
79
|
+
if name:
|
|
80
|
+
name = normalize_entml_name(name)
|
|
81
|
+
if name and not _is_placeholder_invoke_name(name):
|
|
82
|
+
return pos
|
|
83
|
+
search_from = close + 1
|
|
84
|
+
|
|
85
|
+
|
|
29
86
|
def find_complete_entml_invoke_open(buffer: str) -> int:
|
|
30
87
|
"""返回首个含 name 且已闭合 ``>`` 的 ``<entml:invoke`` 起始下标;否则 -1。"""
|
|
31
88
|
if not buffer:
|
|
@@ -59,9 +116,12 @@ def find_ambiguous_entml_tool_prefix(buffer: str) -> int:
|
|
|
59
116
|
|
|
60
117
|
|
|
61
118
|
def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
|
|
62
|
-
"""thinking
|
|
119
|
+
"""thinking 块内:从最早工具 entml 前缀处 hold,避免 invoke 被当作 thinking 吐出。"""
|
|
63
120
|
if not buffer:
|
|
64
121
|
return "", ""
|
|
122
|
+
ambig = find_ambiguous_entml_tool_prefix(buffer)
|
|
123
|
+
if ambig >= 0:
|
|
124
|
+
return buffer[:ambig], buffer[ambig:]
|
|
65
125
|
lt = buffer.rfind("<")
|
|
66
126
|
if lt < 0:
|
|
67
127
|
return buffer, ""
|
|
@@ -103,6 +163,25 @@ def has_unclosed_entml_thinking(text: str) -> bool:
|
|
|
103
163
|
return False
|
|
104
164
|
|
|
105
165
|
|
|
166
|
+
def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
|
|
167
|
+
"""``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
|
|
168
|
+
|
|
169
|
+
已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
|
|
170
|
+
"""
|
|
171
|
+
if invoke_at < 0:
|
|
172
|
+
return False
|
|
173
|
+
think_open = text.find(_THINKING_OPEN_PREFIX)
|
|
174
|
+
if think_open < 0 or invoke_at <= think_open:
|
|
175
|
+
return False
|
|
176
|
+
entml_close = text.find(_THINKING_CLOSE, think_open)
|
|
177
|
+
if entml_close >= 0:
|
|
178
|
+
return invoke_at < entml_close
|
|
179
|
+
fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
|
|
180
|
+
if fault_at >= 0:
|
|
181
|
+
return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
106
185
|
def split_entml_thinking(text: str) -> Tuple[str, str]:
|
|
107
186
|
"""从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
|
|
108
187
|
if not text:
|
|
@@ -147,6 +226,13 @@ class EntmlThinkingStreamFilter:
|
|
|
147
226
|
self._fault_watch = False
|
|
148
227
|
self._fault_buffer = ""
|
|
149
228
|
|
|
229
|
+
def clear_invoke_handoff_state(self) -> None:
|
|
230
|
+
"""invoke 已交给正文/工具解析后,结束 thinking / fault 观察状态。"""
|
|
231
|
+
self._in_block = False
|
|
232
|
+
self._fault_watch = False
|
|
233
|
+
self._fault_buffer = ""
|
|
234
|
+
self._thinking_started = False
|
|
235
|
+
|
|
150
236
|
def in_open_thinking(self) -> bool:
|
|
151
237
|
"""思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
|
|
152
238
|
if self._in_block or self._fault_watch:
|
|
@@ -200,14 +286,56 @@ class EntmlThinkingStreamFilter:
|
|
|
200
286
|
return True
|
|
201
287
|
return False
|
|
202
288
|
|
|
289
|
+
def _try_emit_actionable_invoke_from_in_block(
|
|
290
|
+
self, out: List[Tuple[str, str]]
|
|
291
|
+
) -> bool:
|
|
292
|
+
"""thinking 块内出现真实 invoke 时,在该处结束 thinking 并交给正文解析。"""
|
|
293
|
+
split = find_actionable_invoke_block(self._pending)
|
|
294
|
+
if split is not None:
|
|
295
|
+
before, invoke_block, after = split
|
|
296
|
+
if before:
|
|
297
|
+
emitted = self._emit_thinking_piece(before)
|
|
298
|
+
if emitted:
|
|
299
|
+
out.append(("thinking", emitted))
|
|
300
|
+
out.append(("content", invoke_block))
|
|
301
|
+
self._in_block = False
|
|
302
|
+
self._fault_watch = False
|
|
303
|
+
self._fault_buffer = ""
|
|
304
|
+
self._thinking_started = False
|
|
305
|
+
rest = after.lstrip()
|
|
306
|
+
if rest.startswith(_THINKING_CLOSE):
|
|
307
|
+
rest = rest[len(_THINKING_CLOSE) :].lstrip()
|
|
308
|
+
self._pending = rest
|
|
309
|
+
return True
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def _emit_piece_with_actionable_invokes(
|
|
313
|
+
self, piece: str, out: List[Tuple[str, str]]
|
|
314
|
+
) -> str:
|
|
315
|
+
"""从 thinking 正文中切出真实 invoke 块,返回剩余 thinking 文本。"""
|
|
316
|
+
while piece:
|
|
317
|
+
split = find_actionable_invoke_block(piece)
|
|
318
|
+
if split is None:
|
|
319
|
+
break
|
|
320
|
+
before, invoke_block, rest = split
|
|
321
|
+
if before:
|
|
322
|
+
emitted = self._emit_thinking_piece(before)
|
|
323
|
+
if emitted:
|
|
324
|
+
out.append(("thinking", emitted))
|
|
325
|
+
out.append(("content", invoke_block))
|
|
326
|
+
piece = rest
|
|
327
|
+
return piece
|
|
328
|
+
|
|
203
329
|
def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
204
330
|
"""处理块内 pending;返回 False 表示应退出 feed 主循环。"""
|
|
205
331
|
close_at = self._pending.find(_THINKING_CLOSE)
|
|
206
332
|
if close_at >= 0:
|
|
207
333
|
piece = self._pending[:close_at]
|
|
208
|
-
|
|
209
|
-
if
|
|
210
|
-
|
|
334
|
+
piece = self._emit_piece_with_actionable_invokes(piece, out)
|
|
335
|
+
if piece:
|
|
336
|
+
emitted = self._emit_thinking_piece(piece)
|
|
337
|
+
if emitted:
|
|
338
|
+
out.append(("thinking", emitted))
|
|
211
339
|
self._in_block = False
|
|
212
340
|
self._fault_watch = False
|
|
213
341
|
self._fault_buffer = ""
|
|
@@ -215,6 +343,9 @@ class EntmlThinkingStreamFilter:
|
|
|
215
343
|
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
216
344
|
return True
|
|
217
345
|
|
|
346
|
+
if self._try_emit_actionable_invoke_from_in_block(out):
|
|
347
|
+
return True
|
|
348
|
+
|
|
218
349
|
fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
|
|
219
350
|
if fault_at >= 0:
|
|
220
351
|
before = self._pending[:fault_at]
|
|
@@ -318,10 +449,11 @@ class EntmlThinkingStreamFilter:
|
|
|
318
449
|
self._in_block = False
|
|
319
450
|
self._thinking_started = False
|
|
320
451
|
if self._in_block:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
452
|
+
if not self._try_emit_actionable_invoke_from_in_block(out):
|
|
453
|
+
emitted = self._emit_thinking_piece(self._pending)
|
|
454
|
+
if emitted:
|
|
455
|
+
out.append(("thinking", emitted))
|
|
456
|
+
self._pending = ""
|
|
325
457
|
self._in_block = False
|
|
326
458
|
self._thinking_started = False
|
|
327
459
|
if self._pending:
|
|
@@ -182,7 +182,9 @@ src/tests/test_coverage_core.py
|
|
|
182
182
|
src/tests/test_dispatch.py
|
|
183
183
|
src/tests/test_dispatch_extended.py
|
|
184
184
|
src/tests/test_dispatch_race.py
|
|
185
|
+
src/tests/test_entml_agent_output_formats.py
|
|
185
186
|
src/tests/test_entml_bare_invoke_format.py
|
|
187
|
+
src/tests/test_entml_parser_comprehensive.py
|
|
186
188
|
src/tests/test_entml_protocol.py
|
|
187
189
|
src/tests/test_entml_stream_json.py
|
|
188
190
|
src/tests/test_entml_stream_json_exhaustive.py
|