echotools 2.3.50__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.50 → echotools-2.3.51}/PKG-INFO +1 -1
- {echotools-2.3.50 → echotools-2.3.51}/pyproject.toml +1 -1
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/stream.py +113 -18
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/core.py +1 -1
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/parse.py +122 -9
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_agent_output_formats.py +4 -4
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_parser_comprehensive.py +15 -15
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_fncall_stream_thinking.py +9 -9
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_stream_realtime_parse.py +3 -3
- {echotools-2.3.50 → echotools-2.3.51}/README.md +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/docs/README.md +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/setup.cfg +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/compat.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools/version.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/SOURCES.txt +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/bench_import.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/conftest.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_completion.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_config.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_console.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_errors.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_events.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_facade.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_ids.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_imports.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_io.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_keys.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_logger.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_merge.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_usage.py +0 -0
- {echotools-2.3.50 → echotools-2.3.51}/src/tests/test_web.py +0 -0
- {echotools-2.3.50 → 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,6 +165,9 @@ 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
|
+
|
|
151
171
|
def _drain_thinking_holdback_to_fncall(self) -> None:
|
|
152
172
|
"""invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
|
|
153
173
|
if self._thinking_filter is None:
|
|
@@ -162,14 +182,47 @@ class FncallStreamParser:
|
|
|
162
182
|
self._drain_thinking_holdback_to_fncall()
|
|
163
183
|
self._detected = True
|
|
164
184
|
self._state = self.IN_FUNCTION_CALLS
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
self._json_stream_encoder = None
|
|
186
|
+
if self._thinking_filter is not None:
|
|
187
|
+
self._thinking_filter.clear_invoke_handoff_state()
|
|
167
188
|
|
|
168
189
|
def _feed_content_waiting(self, text: str) -> None:
|
|
169
190
|
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
170
191
|
if not text:
|
|
171
192
|
return
|
|
172
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
|
|
173
226
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
174
227
|
found, pos = self._protocol.detect_start(text)
|
|
175
228
|
if not found:
|
|
@@ -204,10 +257,24 @@ class FncallStreamParser:
|
|
|
204
257
|
self._drain_thinking_holdback_to_fncall()
|
|
205
258
|
|
|
206
259
|
def _feed_waiting(self, chunk: str) -> None:
|
|
207
|
-
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
260
|
+
"""在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
|
|
208
261
|
combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
|
|
209
262
|
self._waiting_tail = ""
|
|
210
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
|
+
|
|
211
278
|
# 已进入 thinking 块时,块内不做 invoke 检测。
|
|
212
279
|
if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
|
|
213
280
|
self._feed_waiting_thinking_plain(combined)
|
|
@@ -279,29 +346,20 @@ class FncallStreamParser:
|
|
|
279
346
|
|
|
280
347
|
self._raw_buf += chunk
|
|
281
348
|
|
|
282
|
-
if self._state
|
|
349
|
+
if self._state != self.DONE:
|
|
283
350
|
self._feed_waiting(chunk)
|
|
284
|
-
else:
|
|
285
|
-
if self._waiting_tail:
|
|
286
|
-
self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
|
|
287
|
-
self._waiting_tail = ""
|
|
288
|
-
self._drain_thinking_holdback_to_fncall()
|
|
289
|
-
self._fncall_buf += self._normalize_stream_chunk(chunk)
|
|
290
|
-
if self._is_call_closed():
|
|
291
|
-
self._state = self.DONE
|
|
292
351
|
|
|
293
352
|
self._pending_stream_delta = self._poll_streaming_tool_input_delta()
|
|
294
353
|
ready = self.get_ready_tool_calls()
|
|
295
|
-
if ready:
|
|
296
|
-
|
|
297
|
-
self._json_stream_encoder.reset()
|
|
354
|
+
if ready and self._json_stream_encoder is not None:
|
|
355
|
+
self._json_stream_encoder.reset()
|
|
298
356
|
return ready
|
|
299
357
|
|
|
300
358
|
def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
|
|
301
359
|
"""invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
|
|
302
360
|
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
303
361
|
return None
|
|
304
|
-
if self._state not in (self.IN_FUNCTION_CALLS,
|
|
362
|
+
if self._state not in (self.IN_FUNCTION_CALLS,):
|
|
305
363
|
return None
|
|
306
364
|
from echotools.exec.fncall.protocols.entml_stream_json import (
|
|
307
365
|
EntmlInvokeJsonStreamEncoder,
|
|
@@ -326,6 +384,43 @@ class FncallStreamParser:
|
|
|
326
384
|
self._pending_stream_delta = None
|
|
327
385
|
return delta
|
|
328
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
|
+
|
|
329
424
|
def _assembly_for_tool_parse(self) -> str:
|
|
330
425
|
"""可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
|
|
331
426
|
return "".join(self._text_parts) + self._waiting_tail + self._fncall_buf
|
{echotools-2.3.50 → 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.50 → 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, ""
|
|
@@ -166,6 +226,13 @@ class EntmlThinkingStreamFilter:
|
|
|
166
226
|
self._fault_watch = False
|
|
167
227
|
self._fault_buffer = ""
|
|
168
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
|
+
|
|
169
236
|
def in_open_thinking(self) -> bool:
|
|
170
237
|
"""思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
|
|
171
238
|
if self._in_block or self._fault_watch:
|
|
@@ -219,14 +286,56 @@ class EntmlThinkingStreamFilter:
|
|
|
219
286
|
return True
|
|
220
287
|
return False
|
|
221
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
|
+
|
|
222
329
|
def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
223
330
|
"""处理块内 pending;返回 False 表示应退出 feed 主循环。"""
|
|
224
331
|
close_at = self._pending.find(_THINKING_CLOSE)
|
|
225
332
|
if close_at >= 0:
|
|
226
333
|
piece = self._pending[:close_at]
|
|
227
|
-
|
|
228
|
-
if
|
|
229
|
-
|
|
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))
|
|
230
339
|
self._in_block = False
|
|
231
340
|
self._fault_watch = False
|
|
232
341
|
self._fault_buffer = ""
|
|
@@ -234,6 +343,9 @@ class EntmlThinkingStreamFilter:
|
|
|
234
343
|
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
235
344
|
return True
|
|
236
345
|
|
|
346
|
+
if self._try_emit_actionable_invoke_from_in_block(out):
|
|
347
|
+
return True
|
|
348
|
+
|
|
237
349
|
fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
|
|
238
350
|
if fault_at >= 0:
|
|
239
351
|
before = self._pending[:fault_at]
|
|
@@ -337,10 +449,11 @@ class EntmlThinkingStreamFilter:
|
|
|
337
449
|
self._in_block = False
|
|
338
450
|
self._thinking_started = False
|
|
339
451
|
if self._in_block:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 = ""
|
|
344
457
|
self._in_block = False
|
|
345
458
|
self._thinking_started = False
|
|
346
459
|
if self._pending:
|
|
@@ -120,15 +120,15 @@ def test_fault_thinking_close_then_invoke() -> None:
|
|
|
120
120
|
assert json.loads(calls[0]["function"]["arguments"]) == {}
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def
|
|
123
|
+
def test_invoke_inside_unclosed_thinking_parsed_when_complete() -> None:
|
|
124
124
|
text = f"<entml:thinking>\nplan {VALID_INVOKE}\n"
|
|
125
125
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
126
126
|
for i in range(0, len(text), 4):
|
|
127
127
|
parser.feed(text[i : i + 4])
|
|
128
|
-
assert
|
|
129
|
-
assert "cursor_agent_simple.py" in parser.partial_thinking
|
|
128
|
+
assert parser.has_calls
|
|
130
129
|
clean, calls = parser.finalize()
|
|
131
|
-
assert calls ==
|
|
130
|
+
assert len(calls) == 1
|
|
131
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
def test_multiline_parameter_preserved() -> None:
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
13
13
|
|
|
14
14
|
import pytest
|
|
15
15
|
|
|
@@ -278,16 +278,18 @@ def _scenarios() -> List[ParserScenario]:
|
|
|
278
278
|
id="invoke_inside_unclosed_thinking",
|
|
279
279
|
text=f"<entml:thinking>\nplan\n{READ_INVOKE}\n",
|
|
280
280
|
tools=[READ_TOOL],
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
expect_names=("Read",),
|
|
282
|
+
expect_args=({"path": READ_PATH},),
|
|
283
|
+
expect_thinking_contains=("plan",),
|
|
283
284
|
stream_only=True,
|
|
284
285
|
),
|
|
285
286
|
ParserScenario(
|
|
286
287
|
id="invoke_and_example_inside_closed_thinking",
|
|
287
288
|
text=example_in_thinking,
|
|
288
289
|
tools=[READ_TOOL],
|
|
289
|
-
|
|
290
|
-
|
|
290
|
+
expect_names=("Read",),
|
|
291
|
+
expect_args=({"path": READ_PATH},),
|
|
292
|
+
expect_thinking_contains=("格式示例", "$FUNCTION_NAME"),
|
|
291
293
|
stream_only=True,
|
|
292
294
|
),
|
|
293
295
|
ParserScenario(
|
|
@@ -559,29 +561,27 @@ def test_invoke_index_inside_unclosed_thinking() -> None:
|
|
|
559
561
|
assert not invoke_index_inside_unclosed_thinking(closed, pos3)
|
|
560
562
|
|
|
561
563
|
|
|
562
|
-
def
|
|
563
|
-
"""回归:首包很大且含 thinking
|
|
564
|
+
def test_large_chunk_invoke_inside_unclosed_thinking_parsed() -> None:
|
|
565
|
+
"""回归:首包很大且含 thinking 内完整 invoke 时应解析工具。"""
|
|
564
566
|
text = f"<entml:thinking>\nplan\n{READ_INVOKE}\n"
|
|
565
567
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=[READ_TOOL])
|
|
566
568
|
parser.feed(text[:64])
|
|
567
|
-
assert parser.state == FncallStreamParser.WAITING_FOR_TAG
|
|
568
569
|
assert parser._thinking_filter is not None
|
|
569
|
-
assert parser._thinking_filter.in_open_thinking()
|
|
570
570
|
parser.feed(text[64:])
|
|
571
571
|
_, calls = parser.finalize()
|
|
572
|
-
assert calls ==
|
|
573
|
-
assert "
|
|
572
|
+
assert len(calls) == 1
|
|
573
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
574
574
|
|
|
575
575
|
|
|
576
|
-
def
|
|
577
|
-
"""
|
|
576
|
+
def test_batch_thinking_invoke_stream_parses_complete_inside() -> None:
|
|
577
|
+
"""流式与批量均解析 thinking 内已闭合的真实 invoke。"""
|
|
578
578
|
proto = get_protocol("entml")
|
|
579
579
|
inside = f"<entml:thinking>\nplan\n{READ_INVOKE}\n</entml:thinking>\n"
|
|
580
580
|
_, batch_calls = proto.parse(inside, [READ_TOOL])
|
|
581
581
|
assert len(batch_calls) >= 1
|
|
582
582
|
stream_clean, stream_calls, _ = _stream_parse(inside, [READ_TOOL], 8)
|
|
583
|
-
assert len(stream_calls) ==
|
|
584
|
-
assert "
|
|
583
|
+
assert len(stream_calls) == 1
|
|
584
|
+
assert stream_calls[0]["function"]["name"] == "Read"
|
|
585
585
|
|
|
586
586
|
|
|
587
587
|
def test_char_by_char_all_scenarios_with_tools() -> None:
|
|
@@ -55,16 +55,16 @@ def test_split_entml_thinking_ignores_unclosed_block() -> None:
|
|
|
55
55
|
assert INVOKE in content
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
58
|
+
def test_invoke_inside_unclosed_thinking_is_parsed_when_complete() -> None:
|
|
59
|
+
"""thinking 内完整的真实 invoke 应切出并解析(占位符示例除外)。"""
|
|
60
60
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
61
61
|
stream = f"<entml:thinking>\nplan {INVOKE}\n"
|
|
62
62
|
for i in range(0, len(stream), 7):
|
|
63
63
|
parser.feed(stream[i : i + 7])
|
|
64
|
-
assert
|
|
65
|
-
assert INVOKE in parser.partial_thinking or "entml:invoke" in parser.partial_thinking
|
|
64
|
+
assert parser.has_calls
|
|
66
65
|
clean, calls = parser.finalize()
|
|
67
|
-
assert len(calls) ==
|
|
66
|
+
assert len(calls) == 1
|
|
67
|
+
assert calls[0]["function"]["name"] == "get_weather"
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def test_invoke_after_thinking_close_is_parsed() -> None:
|
|
@@ -97,18 +97,18 @@ def test_no_thinking_invoke_still_works() -> None:
|
|
|
97
97
|
assert "Sure" in clean
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def
|
|
100
|
+
def test_stream_thinking_invoke_inside_parsed_when_complete() -> None:
|
|
101
101
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
102
102
|
parser.feed("<entml:thinking>\n")
|
|
103
103
|
parser.feed("line one\n")
|
|
104
104
|
assert "line one" in parser.partial_thinking
|
|
105
105
|
assert not parser.has_calls
|
|
106
106
|
parser.feed(f"mention {INVOKE}\n")
|
|
107
|
-
assert
|
|
108
|
-
assert "entml:invoke" in parser.partial_thinking
|
|
107
|
+
assert parser.has_calls
|
|
109
108
|
parser.feed("</entml:thinking>\nanswer")
|
|
110
109
|
clean, calls = parser.finalize()
|
|
111
|
-
assert len(calls) ==
|
|
110
|
+
assert len(calls) == 1
|
|
111
|
+
assert calls[0]["function"]["name"] == "get_weather"
|
|
112
112
|
assert "answer" in clean
|
|
113
113
|
|
|
114
114
|
|
|
@@ -112,7 +112,7 @@ def test_char_by_char_ready_matches_final_for_all_tool_cases() -> None:
|
|
|
112
112
|
assert _args(incremental) == _args(final) == case.expect_args, case.id
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def
|
|
115
|
+
def test_unclosed_thinking_invoke_parsed_when_complete_inside() -> None:
|
|
116
116
|
proto = get_protocol("entml")
|
|
117
117
|
tools = TOOLS
|
|
118
118
|
invoke = (
|
|
@@ -124,8 +124,8 @@ def test_unclosed_thinking_invoke_not_parsed_until_close() -> None:
|
|
|
124
124
|
ready = []
|
|
125
125
|
for ch in f"<entml:thinking>\nplan {invoke}\n":
|
|
126
126
|
ready.extend(parser.feed(ch))
|
|
127
|
-
assert ready == []
|
|
128
|
-
for ch in
|
|
127
|
+
assert _names(ready) == ["get_weather"]
|
|
128
|
+
for ch in "</entml:thinking>\n可见":
|
|
129
129
|
ready.extend(parser.feed(ch))
|
|
130
130
|
ready.extend(parser.get_ready_tool_calls())
|
|
131
131
|
_, final = parser.finalize()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|