echotools 2.3.48__tar.gz → 2.3.50__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.48 → echotools-2.3.50}/PKG-INFO +1 -1
- {echotools-2.3.48 → echotools-2.3.50}/pyproject.toml +1 -1
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/stream.py +32 -10
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history.py +0 -19
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history_format.py +2 -5
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -18
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/parse.py +91 -49
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools.egg-info/SOURCES.txt +2 -0
- echotools-2.3.50/src/tests/test_entml_agent_output_formats.py +143 -0
- echotools-2.3.50/src/tests/test_entml_parser_comprehensive.py +656 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_fncall_stream_thinking.py +79 -11
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_stream_realtime_parse.py +4 -5
- {echotools-2.3.48 → echotools-2.3.50}/README.md +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/docs/README.md +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/setup.cfg +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/compat.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools/version.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/bench_import.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/conftest.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_completion.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_config.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_console.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_errors.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_events.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_facade.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_ids.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_imports.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_io.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_keys.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_logger.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_merge.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_usage.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_web.py +0 -0
- {echotools-2.3.48 → echotools-2.3.50}/src/tests/test_web_broker.py +0 -0
|
@@ -148,6 +148,23 @@ class FncallStreamParser:
|
|
|
148
148
|
return fn(text)
|
|
149
149
|
return text
|
|
150
150
|
|
|
151
|
+
def _drain_thinking_holdback_to_fncall(self) -> None:
|
|
152
|
+
"""invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
|
|
153
|
+
if self._thinking_filter is None:
|
|
154
|
+
return
|
|
155
|
+
pending = self._thinking_filter._pending
|
|
156
|
+
if pending:
|
|
157
|
+
self._fncall_buf += pending
|
|
158
|
+
self._thinking_filter._pending = ""
|
|
159
|
+
|
|
160
|
+
def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
|
|
161
|
+
self._fncall_buf = buffer_from[pos:]
|
|
162
|
+
self._drain_thinking_holdback_to_fncall()
|
|
163
|
+
self._detected = True
|
|
164
|
+
self._state = self.IN_FUNCTION_CALLS
|
|
165
|
+
if self._is_call_closed():
|
|
166
|
+
self._state = self.DONE
|
|
167
|
+
|
|
151
168
|
def _feed_content_waiting(self, text: str) -> None:
|
|
152
169
|
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
153
170
|
if not text:
|
|
@@ -173,11 +190,7 @@ class FncallStreamParser:
|
|
|
173
190
|
if pos > 0:
|
|
174
191
|
self._append_content_text(text[:pos])
|
|
175
192
|
|
|
176
|
-
self.
|
|
177
|
-
self._detected = True
|
|
178
|
-
self._state = self.IN_FUNCTION_CALLS
|
|
179
|
-
if self._is_call_closed():
|
|
180
|
-
self._state = self.DONE
|
|
193
|
+
self._begin_function_calls(text, pos=pos)
|
|
181
194
|
|
|
182
195
|
def _feed_waiting_thinking_plain(self, combined: str) -> None:
|
|
183
196
|
"""未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
|
|
@@ -187,6 +200,8 @@ class FncallStreamParser:
|
|
|
187
200
|
self._thinking_parts.append(part)
|
|
188
201
|
elif part:
|
|
189
202
|
self._feed_content_waiting(part)
|
|
203
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
204
|
+
self._drain_thinking_holdback_to_fncall()
|
|
190
205
|
|
|
191
206
|
def _feed_waiting(self, chunk: str) -> None:
|
|
192
207
|
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
@@ -202,14 +217,17 @@ class FncallStreamParser:
|
|
|
202
217
|
found, pos = self._protocol.detect_start(combined)
|
|
203
218
|
|
|
204
219
|
if found:
|
|
220
|
+
if self._thinking_filter is not None:
|
|
221
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
222
|
+
invoke_index_inside_unclosed_thinking,
|
|
223
|
+
)
|
|
224
|
+
if invoke_index_inside_unclosed_thinking(combined, pos):
|
|
225
|
+
self._feed_waiting_thinking_plain(combined)
|
|
226
|
+
return
|
|
205
227
|
if pos > 0:
|
|
206
228
|
# 工具标签前的正文仍可能含 thinking,走过滤器。
|
|
207
229
|
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
|
|
230
|
+
self._begin_function_calls(combined, pos=pos)
|
|
213
231
|
return
|
|
214
232
|
|
|
215
233
|
# 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
|
|
@@ -264,6 +282,10 @@ class FncallStreamParser:
|
|
|
264
282
|
if self._state == self.WAITING_FOR_TAG:
|
|
265
283
|
self._feed_waiting(chunk)
|
|
266
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()
|
|
267
289
|
self._fncall_buf += self._normalize_stream_chunk(chunk)
|
|
268
290
|
if self._is_call_closed():
|
|
269
291
|
self._state = self.DONE
|
|
@@ -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.48 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -15,6 +15,7 @@ THINKING_BLOCK_RE = re.compile(
|
|
|
15
15
|
|
|
16
16
|
_THINKING_OPEN_PREFIX = "<entml:thinking"
|
|
17
17
|
_THINKING_CLOSE = "</entml:thinking>"
|
|
18
|
+
_FAULT_THINKING_CLOSE = "</thinking>"
|
|
18
19
|
# 与 thinking 共享 `<entml:` 前缀的其它标签:歧义 holdback 应交由工具流式状态机处理
|
|
19
20
|
_AMBIGUOUS_ENTML_PREFIXES = (
|
|
20
21
|
"<entml:invoke",
|
|
@@ -102,6 +103,25 @@ def has_unclosed_entml_thinking(text: str) -> bool:
|
|
|
102
103
|
return False
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
|
|
107
|
+
"""``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
|
|
108
|
+
|
|
109
|
+
已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
|
|
110
|
+
"""
|
|
111
|
+
if invoke_at < 0:
|
|
112
|
+
return False
|
|
113
|
+
think_open = text.find(_THINKING_OPEN_PREFIX)
|
|
114
|
+
if think_open < 0 or invoke_at <= think_open:
|
|
115
|
+
return False
|
|
116
|
+
entml_close = text.find(_THINKING_CLOSE, think_open)
|
|
117
|
+
if entml_close >= 0:
|
|
118
|
+
return invoke_at < entml_close
|
|
119
|
+
fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
|
|
120
|
+
if fault_at >= 0:
|
|
121
|
+
return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
105
125
|
def split_entml_thinking(text: str) -> Tuple[str, str]:
|
|
106
126
|
"""从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
|
|
107
127
|
if not text:
|
|
@@ -142,10 +162,13 @@ class EntmlThinkingStreamFilter:
|
|
|
142
162
|
self._in_block = False
|
|
143
163
|
# 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
|
|
144
164
|
self._thinking_started = False
|
|
165
|
+
# 见到 ``</thinking>`` 后暂存,仅当后续出现 invoke 才视为闭合;否则当纯文本
|
|
166
|
+
self._fault_watch = False
|
|
167
|
+
self._fault_buffer = ""
|
|
145
168
|
|
|
146
169
|
def in_open_thinking(self) -> bool:
|
|
147
|
-
"""思考块已开始且尚未收到
|
|
148
|
-
if self._in_block:
|
|
170
|
+
"""思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
|
|
171
|
+
if self._in_block or self._fault_watch:
|
|
149
172
|
return True
|
|
150
173
|
open_at = self._pending.find(_THINKING_OPEN_PREFIX)
|
|
151
174
|
if open_at >= 0:
|
|
@@ -157,47 +180,47 @@ class EntmlThinkingStreamFilter:
|
|
|
157
180
|
return True
|
|
158
181
|
return False
|
|
159
182
|
|
|
160
|
-
def
|
|
161
|
-
"""thinking
|
|
162
|
-
|
|
163
|
-
if tool_at < 0:
|
|
183
|
+
def _try_resolve_fault_watch_on_invoke(self) -> bool:
|
|
184
|
+
"""``</thinking>`` 之后若出现完整 invoke 开标签,则在该处结束 thinking。"""
|
|
185
|
+
if not self._fault_watch or not self._fault_buffer:
|
|
164
186
|
return False
|
|
165
|
-
|
|
166
|
-
if
|
|
187
|
+
invoke_at = find_complete_entml_invoke_open(self._fault_buffer)
|
|
188
|
+
if invoke_at < len(_FAULT_THINKING_CLOSE):
|
|
167
189
|
return False
|
|
168
|
-
if tool_at > 0:
|
|
169
|
-
emitted = self._emit_thinking_piece(self._pending[:tool_at])
|
|
170
|
-
if emitted:
|
|
171
|
-
out.append(("thinking", emitted))
|
|
172
190
|
self._in_block = False
|
|
191
|
+
self._fault_watch = False
|
|
173
192
|
self._thinking_started = False
|
|
174
|
-
self._pending = self.
|
|
193
|
+
self._pending = self._fault_buffer[len(_FAULT_THINKING_CLOSE) :]
|
|
194
|
+
self._fault_buffer = ""
|
|
175
195
|
return True
|
|
176
196
|
|
|
177
|
-
def
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
if invoke_at < 0:
|
|
197
|
+
def _try_resolve_fault_watch_on_entml_close(self, out: List[Tuple[str, str]]) -> bool:
|
|
198
|
+
"""未见 invoke 时出现 ``</entml:thinking>`` → ``</thinking>`` 仅为思考内纯文本。"""
|
|
199
|
+
if not self._fault_watch:
|
|
181
200
|
return False
|
|
182
|
-
close_at = self.
|
|
183
|
-
if close_at
|
|
201
|
+
close_at = self._fault_buffer.find(_THINKING_CLOSE)
|
|
202
|
+
if close_at < 0:
|
|
184
203
|
return False
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
out.append(("thinking", emitted))
|
|
204
|
+
emitted = self._emit_thinking_piece(self._fault_buffer[:close_at])
|
|
205
|
+
if emitted:
|
|
206
|
+
out.append(("thinking", emitted))
|
|
189
207
|
self._in_block = False
|
|
208
|
+
self._fault_watch = False
|
|
190
209
|
self._thinking_started = False
|
|
191
|
-
self._pending = self.
|
|
210
|
+
self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
|
|
211
|
+
self._fault_buffer = ""
|
|
192
212
|
return True
|
|
193
213
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
196
|
-
if self.
|
|
214
|
+
def _feed_fault_watch(self, out: List[Tuple[str, str]]) -> bool:
|
|
215
|
+
"""处理 ``</thinking>`` 容错观察期;返回 True 表示可继续主循环。"""
|
|
216
|
+
if self._try_resolve_fault_watch_on_invoke():
|
|
197
217
|
return True
|
|
198
|
-
if self.
|
|
218
|
+
if self._try_resolve_fault_watch_on_entml_close(out):
|
|
199
219
|
return True
|
|
220
|
+
return False
|
|
200
221
|
|
|
222
|
+
def _feed_in_block(self, out: List[Tuple[str, str]]) -> bool:
|
|
223
|
+
"""处理块内 pending;返回 False 表示应退出 feed 主循环。"""
|
|
201
224
|
close_at = self._pending.find(_THINKING_CLOSE)
|
|
202
225
|
if close_at >= 0:
|
|
203
226
|
piece = self._pending[:close_at]
|
|
@@ -205,10 +228,23 @@ class EntmlThinkingStreamFilter:
|
|
|
205
228
|
if emitted:
|
|
206
229
|
out.append(("thinking", emitted))
|
|
207
230
|
self._in_block = False
|
|
231
|
+
self._fault_watch = False
|
|
232
|
+
self._fault_buffer = ""
|
|
208
233
|
self._thinking_started = False
|
|
209
234
|
self._pending = self._pending[close_at + len(_THINKING_CLOSE) :]
|
|
210
235
|
return True
|
|
211
236
|
|
|
237
|
+
fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
|
|
238
|
+
if fault_at >= 0:
|
|
239
|
+
before = self._pending[:fault_at]
|
|
240
|
+
emitted = self._emit_thinking_piece(before)
|
|
241
|
+
if emitted:
|
|
242
|
+
out.append(("thinking", emitted))
|
|
243
|
+
self._fault_watch = True
|
|
244
|
+
self._fault_buffer = self._pending[fault_at:]
|
|
245
|
+
self._pending = ""
|
|
246
|
+
return False
|
|
247
|
+
|
|
212
248
|
safe, tool_hold = _hold_ambiguous_tool_markup(self._pending)
|
|
213
249
|
if tool_hold:
|
|
214
250
|
if safe:
|
|
@@ -219,6 +255,8 @@ class EntmlThinkingStreamFilter:
|
|
|
219
255
|
return False
|
|
220
256
|
|
|
221
257
|
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
258
|
+
if not hold:
|
|
259
|
+
safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
|
|
222
260
|
if safe:
|
|
223
261
|
emitted = self._emit_thinking_piece(safe)
|
|
224
262
|
if emitted:
|
|
@@ -257,6 +295,12 @@ class EntmlThinkingStreamFilter:
|
|
|
257
295
|
out: List[Tuple[str, str]] = []
|
|
258
296
|
|
|
259
297
|
while self._pending:
|
|
298
|
+
if self._fault_watch:
|
|
299
|
+
self._fault_buffer += self._pending
|
|
300
|
+
self._pending = ""
|
|
301
|
+
if self._feed_fault_watch(out):
|
|
302
|
+
continue
|
|
303
|
+
break
|
|
260
304
|
if self._in_block:
|
|
261
305
|
if not self._feed_in_block(out):
|
|
262
306
|
break
|
|
@@ -279,39 +323,37 @@ class EntmlThinkingStreamFilter:
|
|
|
279
323
|
|
|
280
324
|
def finalize(self) -> List[Tuple[str, str]]:
|
|
281
325
|
out: List[Tuple[str, str]] = []
|
|
326
|
+
if self._fault_watch:
|
|
327
|
+
if self._try_resolve_fault_watch_on_invoke():
|
|
328
|
+
pass
|
|
329
|
+
elif self._try_resolve_fault_watch_on_entml_close(out):
|
|
330
|
+
pass
|
|
331
|
+
else:
|
|
332
|
+
emitted = self._emit_thinking_piece(self._fault_buffer)
|
|
333
|
+
if emitted:
|
|
334
|
+
out.append(("thinking", emitted))
|
|
335
|
+
self._fault_buffer = ""
|
|
336
|
+
self._fault_watch = False
|
|
337
|
+
self._in_block = False
|
|
338
|
+
self._thinking_started = False
|
|
282
339
|
if self._in_block:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
emitted = self._emit_thinking_piece(self._pending)
|
|
287
|
-
if emitted:
|
|
288
|
-
out.append(("thinking", emitted))
|
|
289
|
-
self._pending = ""
|
|
290
|
-
self._in_block = False
|
|
291
|
-
self._thinking_started = False
|
|
292
|
-
return out
|
|
293
|
-
# invoke 已切出,继续处理 _pending 中的正文
|
|
294
|
-
while self._pending:
|
|
295
|
-
if self._in_block:
|
|
296
|
-
if not self._feed_in_block(out):
|
|
297
|
-
break
|
|
298
|
-
continue
|
|
299
|
-
if not self._feed_before_block(out):
|
|
300
|
-
break
|
|
340
|
+
emitted = self._emit_thinking_piece(self._pending)
|
|
341
|
+
if emitted:
|
|
342
|
+
out.append(("thinking", emitted))
|
|
301
343
|
self._pending = ""
|
|
302
344
|
self._in_block = False
|
|
303
345
|
self._thinking_started = False
|
|
304
|
-
|
|
305
|
-
elif self._pending:
|
|
346
|
+
if self._pending:
|
|
306
347
|
content, thinking = split_entml_thinking(self._pending)
|
|
307
348
|
if thinking:
|
|
308
349
|
out.append(("thinking", thinking))
|
|
309
350
|
if content:
|
|
310
351
|
out.append(("content", content))
|
|
311
352
|
elif self._pending and not thinking:
|
|
312
|
-
# 仅 hold 了半截开标签:原样吐出,避免吞字
|
|
313
353
|
out.append(("content", self._pending))
|
|
314
354
|
self._pending = ""
|
|
315
355
|
self._in_block = False
|
|
356
|
+
self._fault_watch = False
|
|
357
|
+
self._fault_buffer = ""
|
|
316
358
|
self._thinking_started = False
|
|
317
359
|
return out
|
|
@@ -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
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""模型/agent 常见输出格式:正确 entml:invoke vs 错误 JSON/旧格式。"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from echotools.exec.fncall import get_protocol
|
|
10
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
11
|
+
|
|
12
|
+
TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
"type": "function",
|
|
15
|
+
"function": {
|
|
16
|
+
"name": "Read",
|
|
17
|
+
"description": "Read a file",
|
|
18
|
+
"parameters": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {"path": {"type": "string"}},
|
|
21
|
+
"required": ["path"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "function",
|
|
27
|
+
"function": {
|
|
28
|
+
"name": "get_time",
|
|
29
|
+
"description": "Get current time",
|
|
30
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
VALID_INVOKE = (
|
|
36
|
+
'<entml:invoke name="Read">\n'
|
|
37
|
+
'<entml:parameter name="path">C:/Users/Administrator/cursor_agent_simple.py</entml:parameter>\n'
|
|
38
|
+
"</entml:invoke>"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
WRONG_JSON_TOOL = (
|
|
42
|
+
'{"name": "Read", "arguments": {"path": "C:/Users/Administrator/cursor_agent_simple.py"}}'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
WRONG_TOOL_BLOCK = (
|
|
46
|
+
"<tool>\n"
|
|
47
|
+
'{"name": "Read", "arguments": {"path": "x.py"}}\n'
|
|
48
|
+
"</tool>"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
FAKE_PARTIAL_JSON = '{"name": "Read", "arguments": {"path":'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_batch(text: str):
|
|
55
|
+
proto = get_protocol("entml")
|
|
56
|
+
clean, calls = proto.parse(text, TOOLS)
|
|
57
|
+
return clean, calls
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_stream(text: str, *, chunk: int = 3):
|
|
61
|
+
proto = get_protocol("entml")
|
|
62
|
+
parser = FncallStreamParser(protocol=proto, tools=TOOLS)
|
|
63
|
+
for i in range(0, len(text), chunk):
|
|
64
|
+
parser.feed(text[i : i + chunk])
|
|
65
|
+
return parser.finalize()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("chunk", [1, 4, 17])
|
|
69
|
+
def test_valid_entml_invoke_batch_and_stream(chunk: int) -> None:
|
|
70
|
+
text = f"我先读文件。\n{VALID_INVOKE}\n完成。"
|
|
71
|
+
clean, calls = _parse_batch(text)
|
|
72
|
+
assert len(calls) == 1
|
|
73
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
74
|
+
assert "cursor_agent_simple.py" in calls[0]["function"]["arguments"]
|
|
75
|
+
assert "entml:invoke" not in clean
|
|
76
|
+
|
|
77
|
+
clean_s, calls_s = _parse_stream(text, chunk=chunk)
|
|
78
|
+
assert len(calls_s) == 1
|
|
79
|
+
assert calls_s[0]["function"]["name"] == "Read"
|
|
80
|
+
assert "entml:invoke" not in clean_s
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_wrong_json_object_not_parsed_as_tool() -> None:
|
|
84
|
+
text = f"调用工具:\n{WRONG_JSON_TOOL}"
|
|
85
|
+
clean, calls = _parse_batch(text)
|
|
86
|
+
assert calls == []
|
|
87
|
+
assert "Read" in clean or "name" in clean
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_wrong_tool_block_not_parsed_as_entml_invoke() -> None:
|
|
91
|
+
text = f"回复\n{WRONG_TOOL_BLOCK}"
|
|
92
|
+
clean, calls = _parse_batch(text)
|
|
93
|
+
assert calls == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_truncated_json_not_parsed() -> None:
|
|
97
|
+
clean, calls = _parse_batch(FAKE_PARTIAL_JSON)
|
|
98
|
+
assert calls == []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_thinking_then_valid_invoke(chunk: int = 5) -> None:
|
|
102
|
+
text = (
|
|
103
|
+
"<entml:thinking>\n需要先读脚本\n</entml:thinking>\n"
|
|
104
|
+
f"{VALID_INVOKE}"
|
|
105
|
+
)
|
|
106
|
+
clean, calls = _parse_stream(text, chunk=chunk)
|
|
107
|
+
assert len(calls) == 1
|
|
108
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_fault_thinking_close_then_invoke() -> None:
|
|
112
|
+
text = (
|
|
113
|
+
"<entml:thinking>\nplan\n</thinking>\n"
|
|
114
|
+
'<entml:invoke name="get_time">\n'
|
|
115
|
+
"</entml:invoke>"
|
|
116
|
+
)
|
|
117
|
+
clean, calls = _parse_stream(text, chunk=2)
|
|
118
|
+
assert len(calls) == 1
|
|
119
|
+
assert calls[0]["function"]["name"] == "get_time"
|
|
120
|
+
assert json.loads(calls[0]["function"]["arguments"]) == {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
|
|
124
|
+
text = f"<entml:thinking>\nplan {VALID_INVOKE}\n"
|
|
125
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
126
|
+
for i in range(0, len(text), 4):
|
|
127
|
+
parser.feed(text[i : i + 4])
|
|
128
|
+
assert not parser.has_calls
|
|
129
|
+
assert "cursor_agent_simple.py" in parser.partial_thinking
|
|
130
|
+
clean, calls = parser.finalize()
|
|
131
|
+
assert calls == []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_multiline_parameter_preserved() -> None:
|
|
135
|
+
body = "line1\nline2\nline3"
|
|
136
|
+
text = (
|
|
137
|
+
'<entml:invoke name="Read">\n'
|
|
138
|
+
f'<entml:parameter name="path">{body}</entml:parameter>\n'
|
|
139
|
+
"</entml:invoke>"
|
|
140
|
+
)
|
|
141
|
+
_, calls = _parse_batch(text)
|
|
142
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
143
|
+
assert args["path"] == body
|