echotools 2.3.88__tar.gz → 2.3.90__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.88 → echotools-2.3.90}/PKG-INFO +1 -1
- {echotools-2.3.88 → echotools-2.3.90}/pyproject.toml +1 -1
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream.py +5 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_delta.py +5 -5
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_feed.py +5 -5
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_final.py +27 -6
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/templates.py +16 -6
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml.py +47 -29
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_invoke.py +5 -1
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_patterns.py +109 -25
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/body.py +14 -2
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/hold.py +17 -23
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/parse.py +29 -13
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/protocol/base.py +5 -1
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/SOURCES.txt +1 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_bare_invoke_format.py +30 -9
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_batch_stream_comprehensive.py +2 -1
- echotools-2.3.90/src/tests/test_entml_invoke_known_tool_gate.py +220 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_protocol.py +4 -4
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tool_pipeline.py +3 -3
- {echotools-2.3.88 → echotools-2.3.90}/README.md +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/setup.cfg +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/compat.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_schema.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/blocks.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools/version.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/bench_import.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/conftest.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_completion.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_config.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_console.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_errors.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_events.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_facade.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_ids.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_imports.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_io.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_keys.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_logger.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_merge.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_stream_partial_json_parity.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_usage.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_web.py +0 -0
- {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_web_broker.py +0 -0
|
@@ -100,6 +100,11 @@ class FncallStreamParser(StreamFeedMixin, StreamDeltaMixin, StreamFinalMixin):
|
|
|
100
100
|
self._schema_index = _build_param_schema_index(self._tools)
|
|
101
101
|
return self._schema_index
|
|
102
102
|
|
|
103
|
+
def _get_known_tool_names(self):
|
|
104
|
+
from echotools.exec.fncall.protocols.entml_patterns import resolve_known_tool_names
|
|
105
|
+
|
|
106
|
+
return resolve_known_tool_names(self._tools, self._get_schema_index())
|
|
107
|
+
|
|
103
108
|
def _emit_text(self, text: str) -> None:
|
|
104
109
|
"""将文本路由给思考过滤器(若启用)或直接追加到 _text_parts。"""
|
|
105
110
|
if not text:
|
|
@@ -26,7 +26,7 @@ class StreamDeltaMixin:
|
|
|
26
26
|
|
|
27
27
|
complete = list(INVOKE_RE.finditer(self._fncall_buf))
|
|
28
28
|
complete_n = len(complete)
|
|
29
|
-
parsed = split_invoke_open(self._fncall_buf)
|
|
29
|
+
parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
|
|
30
30
|
if parsed is None:
|
|
31
31
|
return max(0, complete_n - 1)
|
|
32
32
|
_name, body_start = parsed
|
|
@@ -77,7 +77,7 @@ class StreamDeltaMixin:
|
|
|
77
77
|
build_streaming_json_snapshot,
|
|
78
78
|
split_invoke_open,
|
|
79
79
|
)
|
|
80
|
-
parsed = split_invoke_open(self._fncall_buf)
|
|
80
|
+
parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
|
|
81
81
|
if not parsed:
|
|
82
82
|
self._json_stream_encoder.reset()
|
|
83
83
|
return
|
|
@@ -125,7 +125,7 @@ class StreamDeltaMixin:
|
|
|
125
125
|
split_invoke_open,
|
|
126
126
|
)
|
|
127
127
|
|
|
128
|
-
parsed = split_invoke_open(self._fncall_buf)
|
|
128
|
+
parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
|
|
129
129
|
if not parsed:
|
|
130
130
|
self._json_stream_encoder.reset()
|
|
131
131
|
return
|
|
@@ -172,7 +172,7 @@ class StreamDeltaMixin:
|
|
|
172
172
|
split_invoke_open,
|
|
173
173
|
)
|
|
174
174
|
|
|
175
|
-
parsed = split_invoke_open(self._fncall_buf)
|
|
175
|
+
parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
|
|
176
176
|
if not parsed:
|
|
177
177
|
return None
|
|
178
178
|
name, body_start = parsed
|
|
@@ -241,7 +241,7 @@ class StreamDeltaMixin:
|
|
|
241
241
|
split_invoke_open,
|
|
242
242
|
)
|
|
243
243
|
|
|
244
|
-
parsed = split_invoke_open(self._fncall_buf)
|
|
244
|
+
parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
|
|
245
245
|
if not parsed:
|
|
246
246
|
return None
|
|
247
247
|
name, body_start = parsed
|
|
@@ -37,7 +37,7 @@ class StreamFeedMixin:
|
|
|
37
37
|
"""True 表示已消费 text(含 holdback)。"""
|
|
38
38
|
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
39
39
|
if hold_from_fn is not None:
|
|
40
|
-
hold_from = hold_from_fn(text)
|
|
40
|
+
hold_from = hold_from_fn(text, tools=self._tools)
|
|
41
41
|
if hold_from is not None:
|
|
42
42
|
if hold_from > 0:
|
|
43
43
|
self._append_content_text(text[:hold_from])
|
|
@@ -52,7 +52,7 @@ class StreamFeedMixin:
|
|
|
52
52
|
def _hold_or_split_emit(self, text: str, trigger_tags) -> None:
|
|
53
53
|
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
54
54
|
if hold_from_fn is not None:
|
|
55
|
-
hold_from = hold_from_fn(text)
|
|
55
|
+
hold_from = hold_from_fn(text, tools=self._tools)
|
|
56
56
|
if hold_from is not None:
|
|
57
57
|
if hold_from > 0:
|
|
58
58
|
self._emit_text(text[:hold_from])
|
|
@@ -81,7 +81,7 @@ class StreamFeedMixin:
|
|
|
81
81
|
self._fncall_buf += text
|
|
82
82
|
return
|
|
83
83
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
84
|
-
found, pos = self._protocol.detect_start(text)
|
|
84
|
+
found, pos = self._protocol.detect_start(text, tools=self._tools)
|
|
85
85
|
if found:
|
|
86
86
|
if pos > 0:
|
|
87
87
|
self._append_content_text(text[:pos])
|
|
@@ -90,7 +90,7 @@ class StreamFeedMixin:
|
|
|
90
90
|
self._hold_or_split_content(text, trigger_tags)
|
|
91
91
|
return
|
|
92
92
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
93
|
-
found, pos = self._protocol.detect_start(text)
|
|
93
|
+
found, pos = self._protocol.detect_start(text, tools=self._tools)
|
|
94
94
|
if not found:
|
|
95
95
|
self._hold_or_split_content(text, trigger_tags)
|
|
96
96
|
return
|
|
@@ -138,7 +138,7 @@ class StreamFeedMixin:
|
|
|
138
138
|
self._feed_waiting_thinking_plain(combined)
|
|
139
139
|
return
|
|
140
140
|
trigger_tags = self._protocol.get_trigger_tags()
|
|
141
|
-
found, pos = self._protocol.detect_start(combined)
|
|
141
|
+
found, pos = self._protocol.detect_start(combined, tools=self._tools)
|
|
142
142
|
if found:
|
|
143
143
|
if self._thinking_filter is not None:
|
|
144
144
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
@@ -67,10 +67,18 @@ class StreamFinalMixin:
|
|
|
67
67
|
|
|
68
68
|
lower = raw.lower()
|
|
69
69
|
cut = len(raw)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
71
|
+
find_actionable_entml_invoke_open,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
invoke_pos = find_actionable_entml_invoke_open(
|
|
75
|
+
raw, known_names=self._get_known_tool_names()
|
|
76
|
+
)
|
|
77
|
+
if invoke_pos >= 0:
|
|
78
|
+
cut = min(cut, invoke_pos)
|
|
79
|
+
legacy_pos = lower.find("<entml:function_calls")
|
|
80
|
+
if legacy_pos >= 0:
|
|
81
|
+
cut = min(cut, legacy_pos)
|
|
74
82
|
segment = raw[:cut]
|
|
75
83
|
|
|
76
84
|
buf = stream_safe_visible_prefix(
|
|
@@ -128,12 +136,25 @@ class StreamFinalMixin:
|
|
|
128
136
|
)
|
|
129
137
|
clean_fn = getattr(self._protocol, "clean_tool_tags", None)
|
|
130
138
|
if callable(clean_fn):
|
|
131
|
-
|
|
139
|
+
import inspect
|
|
140
|
+
|
|
141
|
+
sig = inspect.signature(clean_fn)
|
|
142
|
+
if "tools" in sig.parameters:
|
|
143
|
+
clean_text = clean_fn(clean_text, tools=self._tools)
|
|
144
|
+
else:
|
|
145
|
+
clean_text = clean_fn(clean_text)
|
|
132
146
|
elif getattr(self._protocol, "id", None) == "entml":
|
|
133
147
|
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
148
|
+
resolve_known_tool_names,
|
|
134
149
|
strip_tool_entml_residue,
|
|
135
150
|
)
|
|
136
|
-
|
|
151
|
+
|
|
152
|
+
clean_text = strip_tool_entml_residue(
|
|
153
|
+
clean_text,
|
|
154
|
+
known_names=resolve_known_tool_names(
|
|
155
|
+
self._tools, self._get_schema_index()
|
|
156
|
+
),
|
|
157
|
+
)
|
|
137
158
|
display_text = self._finalize_display_text(clean_text)
|
|
138
159
|
self._text_parts = [display_text] if display_text else []
|
|
139
160
|
self._finalized_result = (display_text, tool_calls)
|
|
@@ -118,14 +118,24 @@ _HISTORY_CLARIFY_EN = (
|
|
|
118
118
|
"The user's latest message follows below."
|
|
119
119
|
)
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
"IMPORTANT
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
121
|
+
_TOOL_INVOKE_REMINDER_EN = (
|
|
122
|
+
"IMPORTANT — Tool invocation format for THIS reply (mandatory):\n"
|
|
123
|
+
"1. To execute a tool, output a literal `<entml:invoke name=\"TOOL_NAME\">` block "
|
|
124
|
+
"exactly as shown at the top of this prompt. The opening tag MUST close with `>`; "
|
|
125
|
+
"`TOOL_NAME` MUST be one of the available functions listed above (not a placeholder "
|
|
126
|
+
"or invented name).\n"
|
|
127
|
+
"2. Each argument goes in "
|
|
128
|
+
"`<entml:parameter name=\"PARAMETER_NAME\">VALUE</entml:parameter>` inside the invoke; "
|
|
129
|
+
"close with `</entml:invoke>`. String/scalar values as-is; lists and objects as JSON.\n"
|
|
130
|
+
"3. Do NOT copy `<tool>` blocks or `{ToolName: ...}` lines from conversation history — "
|
|
131
|
+
"those turns are already executed. History is context only; never re-invoke completed tools.\n"
|
|
132
|
+
"4. When mentioning `<entml:invoke>` in prose without executing, use backticks or otherwise "
|
|
133
|
+
"avoid a closed tag with a real tool name."
|
|
127
134
|
)
|
|
128
135
|
|
|
136
|
+
# Back-compat alias
|
|
137
|
+
_HISTORY_TOOL_INVOKE_REMINDER_EN = _TOOL_INVOKE_REMINDER_EN
|
|
138
|
+
|
|
129
139
|
_HISTORY_CLARIFY_ZH = (
|
|
130
140
|
"以下是已完成的交互记录。"
|
|
131
141
|
"此处展示的所有工具调用及其结果均已执行完毕,请勿重复调用。"
|
|
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
13
13
|
|
|
14
14
|
from echotools.exec.fncall.prompt.templates import (
|
|
15
15
|
_HISTORY_CLARIFY_EN,
|
|
16
|
-
|
|
16
|
+
_TOOL_INVOKE_REMINDER_EN,
|
|
17
17
|
)
|
|
18
18
|
from echotools.exec.fncall.protocols.entml_invoke import (
|
|
19
19
|
parse_entml_tool_calls,
|
|
@@ -25,7 +25,9 @@ from echotools.exec.fncall.protocols.entml_think.blocks import (
|
|
|
25
25
|
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
26
26
|
entml_invoke_open_may_be_streaming,
|
|
27
27
|
extract_attr_value,
|
|
28
|
+
find_actionable_entml_invoke_open,
|
|
28
29
|
normalize_entml_name,
|
|
30
|
+
resolve_known_tool_names,
|
|
29
31
|
strip_actionable_entml_invoke_blocks,
|
|
30
32
|
strip_legacy_function_calls_wrapper,
|
|
31
33
|
strip_tool_entml_residue,
|
|
@@ -192,8 +194,6 @@ class EntmlProtocol(ToolProtocol):
|
|
|
192
194
|
sections.append(
|
|
193
195
|
format_entml_conversation_history(history_text, _HISTORY_CLARIFY_EN)
|
|
194
196
|
)
|
|
195
|
-
if tool_descs and history_has_tool_calls:
|
|
196
|
-
sections.append(_HISTORY_TOOL_INVOKE_REMINDER_EN)
|
|
197
197
|
|
|
198
198
|
if loop_warning:
|
|
199
199
|
sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
|
|
@@ -206,6 +206,9 @@ class EntmlProtocol(ToolProtocol):
|
|
|
206
206
|
if current_user_message is not None:
|
|
207
207
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
208
208
|
|
|
209
|
+
if tool_descs:
|
|
210
|
+
sections.append(_TOOL_INVOKE_REMINDER_EN)
|
|
211
|
+
|
|
209
212
|
# thinking 放在最后,超限截断时优先保留在 send_text 尾部
|
|
210
213
|
thinking_section = build_entml_thinking_section(
|
|
211
214
|
protocol_options, has_tools=bool(tool_descs), history_text=history_text
|
|
@@ -215,16 +218,28 @@ class EntmlProtocol(ToolProtocol):
|
|
|
215
218
|
|
|
216
219
|
return "\n\n".join(sections)
|
|
217
220
|
|
|
218
|
-
def detect_start(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
def detect_start(
|
|
222
|
+
self,
|
|
223
|
+
buffer: str,
|
|
224
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
225
|
+
) -> Tuple[bool, int]:
|
|
226
|
+
"""invoke 开标签(闭合 ``>`` 且 name 在 tools 内)稳定后才视为工具流开始。"""
|
|
227
|
+
schema_index = _build_param_schema_index(tools) if tools else None
|
|
228
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
229
|
+
invoke_pos = find_actionable_entml_invoke_open(buffer, known_names=known)
|
|
221
230
|
if invoke_pos < 0:
|
|
222
231
|
return (False, -1)
|
|
223
232
|
return (True, invoke_pos)
|
|
224
233
|
|
|
225
|
-
def find_fncall_hold_from(
|
|
234
|
+
def find_fncall_hold_from(
|
|
235
|
+
self,
|
|
236
|
+
buffer: str,
|
|
237
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
238
|
+
) -> Optional[int]:
|
|
226
239
|
"""invoke 未稳定前 hold 前缀;legacy 外壳开标签未闭合时同样 hold。"""
|
|
227
|
-
|
|
240
|
+
schema_index = _build_param_schema_index(tools) if tools else None
|
|
241
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
242
|
+
if find_actionable_entml_invoke_open(buffer, known_names=known) >= 0:
|
|
228
243
|
return None
|
|
229
244
|
hold: Optional[int] = None
|
|
230
245
|
legacy_pos = buffer.find(self._LEGACY_WRAPPER_PREFIX)
|
|
@@ -235,28 +250,23 @@ class EntmlProtocol(ToolProtocol):
|
|
|
235
250
|
invoke_pos = buffer.find(self._TRIGGER_PREFIX)
|
|
236
251
|
if (
|
|
237
252
|
invoke_pos >= 0
|
|
238
|
-
and entml_invoke_open_may_be_streaming(
|
|
253
|
+
and entml_invoke_open_may_be_streaming(
|
|
254
|
+
buffer, invoke_pos, known_names=known
|
|
255
|
+
)
|
|
239
256
|
and (hold is None or invoke_pos < hold)
|
|
240
257
|
):
|
|
241
258
|
hold = invoke_pos
|
|
242
259
|
return hold
|
|
243
260
|
|
|
244
|
-
def _find_complete_invoke_open(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if close < 0:
|
|
254
|
-
return -1
|
|
255
|
-
attrs = buffer[pos + prefix_len : close]
|
|
256
|
-
name = extract_attr_value(attrs, "name")
|
|
257
|
-
if name and normalize_entml_name(name):
|
|
258
|
-
return pos
|
|
259
|
-
search_from = close + 1
|
|
261
|
+
def _find_complete_invoke_open(
|
|
262
|
+
self,
|
|
263
|
+
buffer: str,
|
|
264
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
265
|
+
) -> int:
|
|
266
|
+
"""返回首个可解析 ``<entml:invoke`` 起始下标;否则 ``-1``。"""
|
|
267
|
+
schema_index = _build_param_schema_index(tools) if tools else None
|
|
268
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
269
|
+
return find_actionable_entml_invoke_open(buffer, known_names=known)
|
|
260
270
|
|
|
261
271
|
def parse(
|
|
262
272
|
self,
|
|
@@ -282,6 +292,8 @@ class EntmlProtocol(ToolProtocol):
|
|
|
282
292
|
)
|
|
283
293
|
if unclosed_open_at >= 0:
|
|
284
294
|
parse_text = text[:unclosed_open_at]
|
|
295
|
+
schema_index = _build_param_schema_index(tools) if tools else None
|
|
296
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
285
297
|
tool_calls = parse_entml_tool_calls(parse_text, tools, schema_index)
|
|
286
298
|
tool_block_calls: List[Dict[str, Any]] = []
|
|
287
299
|
if include_tool_blocks:
|
|
@@ -297,8 +309,8 @@ class EntmlProtocol(ToolProtocol):
|
|
|
297
309
|
# 须在移除 invoke 之前剥离伪 history,且须晚于已解析的 ``<tool>`` 块剥离。
|
|
298
310
|
clean, _ = strip_fake_history_markup(clean)
|
|
299
311
|
if tool_calls:
|
|
300
|
-
clean = strip_actionable_entml_invoke_blocks(clean)
|
|
301
|
-
clean = strip_tool_entml_residue(clean)
|
|
312
|
+
clean = strip_actionable_entml_invoke_blocks(clean, known_names=known)
|
|
313
|
+
clean = strip_tool_entml_residue(clean, known_names=known)
|
|
302
314
|
return (clean, normalize_tool_calls(tool_calls, tools))
|
|
303
315
|
|
|
304
316
|
def parse_fragment(
|
|
@@ -313,9 +325,15 @@ class EntmlProtocol(ToolProtocol):
|
|
|
313
325
|
"""user 消息:仅去掉 ``entml:`` 前缀,不做其它剥离。"""
|
|
314
326
|
return strip_entml_from_content(content)
|
|
315
327
|
|
|
316
|
-
def clean_tool_tags(
|
|
328
|
+
def clean_tool_tags(
|
|
329
|
+
self,
|
|
330
|
+
content: str,
|
|
331
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
332
|
+
) -> str:
|
|
317
333
|
"""仅剥离工具相关标签残留,保留 thinking;并移除伪 history 块。"""
|
|
318
|
-
|
|
334
|
+
schema_index = _build_param_schema_index(tools) if tools else None
|
|
335
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
336
|
+
cleaned = strip_tool_entml_residue(content, known_names=known)
|
|
319
337
|
cleaned, _ = strip_fake_history_markup(cleaned)
|
|
320
338
|
return cleaned
|
|
321
339
|
|
|
@@ -15,6 +15,7 @@ from .entml_patterns import (
|
|
|
15
15
|
iter_actionable_entml_invoke_blocks,
|
|
16
16
|
normalize_entml_name,
|
|
17
17
|
parse_sub_tags,
|
|
18
|
+
resolve_known_tool_names,
|
|
18
19
|
split_mangled_json_param_tail,
|
|
19
20
|
synthetic_close_invoke_body,
|
|
20
21
|
)
|
|
@@ -143,7 +144,10 @@ def parse_entml_tool_calls(
|
|
|
143
144
|
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
|
|
144
145
|
) -> List[Dict[str, Any]]:
|
|
145
146
|
tool_calls: List[Dict[str, Any]] = []
|
|
146
|
-
|
|
147
|
+
known = resolve_known_tool_names(tools, schema_index)
|
|
148
|
+
for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(
|
|
149
|
+
text, known_names=known
|
|
150
|
+
):
|
|
147
151
|
name = extract_attr_value(attrs, "name")
|
|
148
152
|
if not name:
|
|
149
153
|
continue
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
5
|
+
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple
|
|
6
6
|
|
|
7
7
|
from .entml_schema import coerce_entml_parameter_value
|
|
8
8
|
|
|
@@ -116,8 +116,37 @@ def is_placeholder_invoke_name(name: str) -> bool:
|
|
|
116
116
|
return not n or "$" in n
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def
|
|
120
|
-
|
|
119
|
+
def resolve_known_tool_names(
|
|
120
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
121
|
+
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None,
|
|
122
|
+
) -> Optional[Set[str]]:
|
|
123
|
+
"""从 tools / schema_index 解析已知工具名;无约束时返回 ``None``。"""
|
|
124
|
+
if schema_index:
|
|
125
|
+
return {normalize_entml_name(str(k)) for k in schema_index.keys()}
|
|
126
|
+
names: Set[str] = set()
|
|
127
|
+
for tool in tools or []:
|
|
128
|
+
fn = tool.get("function") or {}
|
|
129
|
+
name = fn.get("name")
|
|
130
|
+
if name:
|
|
131
|
+
names.add(normalize_entml_name(str(name)))
|
|
132
|
+
return names if names else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _name_is_known(name: str, known_names: Optional[Set[str]]) -> bool:
|
|
136
|
+
if known_names is None:
|
|
137
|
+
return True
|
|
138
|
+
if not known_names:
|
|
139
|
+
return False
|
|
140
|
+
return normalize_entml_name(name) in known_names
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def entml_invoke_open_is_actionable(
|
|
144
|
+
buffer: str,
|
|
145
|
+
pos: int,
|
|
146
|
+
*,
|
|
147
|
+
known_names: Optional[Set[str]] = None,
|
|
148
|
+
) -> bool:
|
|
149
|
+
"""``pos`` 处 ``<entml:invoke`` 是否为已闭合、含真实 name 且(若提供)在已知工具表内的开标签。"""
|
|
121
150
|
if pos < 0 or not buffer.startswith(_INVOKE_OPEN_PREFIX, pos):
|
|
122
151
|
return False
|
|
123
152
|
gt = buffer.find(">", pos + len(_INVOKE_OPEN_PREFIX))
|
|
@@ -128,29 +157,60 @@ def entml_invoke_open_is_actionable(buffer: str, pos: int) -> bool:
|
|
|
128
157
|
if not name:
|
|
129
158
|
return False
|
|
130
159
|
name = normalize_entml_name(name)
|
|
131
|
-
|
|
160
|
+
if not name or is_placeholder_invoke_name(name):
|
|
161
|
+
return False
|
|
162
|
+
return _name_is_known(name, known_names)
|
|
132
163
|
|
|
133
164
|
|
|
134
|
-
def entml_invoke_open_may_be_streaming(
|
|
135
|
-
|
|
165
|
+
def entml_invoke_open_may_be_streaming(
|
|
166
|
+
buffer: str,
|
|
167
|
+
pos: int,
|
|
168
|
+
*,
|
|
169
|
+
known_names: Optional[Set[str]] = None,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""``pos`` 处 ``<entml:invoke`` 是否仍可能长成可解析工具开标签(非 prose 提及)。"""
|
|
136
172
|
if pos < 0 or not buffer.startswith(_INVOKE_OPEN_PREFIX, pos):
|
|
137
173
|
return False
|
|
138
|
-
if entml_invoke_open_is_actionable(buffer, pos):
|
|
139
|
-
return True
|
|
140
174
|
gt = buffer.find(">", pos + len(_INVOKE_OPEN_PREFIX))
|
|
141
175
|
if gt < 0:
|
|
142
176
|
return True
|
|
143
|
-
return
|
|
177
|
+
return entml_invoke_open_is_actionable(buffer, pos, known_names=known_names)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def find_actionable_entml_invoke_open(
|
|
181
|
+
buffer: str,
|
|
182
|
+
*,
|
|
183
|
+
known_names: Optional[Set[str]] = None,
|
|
184
|
+
) -> int:
|
|
185
|
+
"""返回首个可解析 ``<entml:invoke`` 起始下标;否则 ``-1``。"""
|
|
186
|
+
if not buffer:
|
|
187
|
+
return -1
|
|
188
|
+
search_from = 0
|
|
189
|
+
prefix_len = len(_INVOKE_OPEN_PREFIX)
|
|
190
|
+
while search_from < len(buffer):
|
|
191
|
+
pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
|
|
192
|
+
if pos < 0:
|
|
193
|
+
return -1
|
|
194
|
+
if entml_invoke_open_is_actionable(buffer, pos, known_names=known_names):
|
|
195
|
+
return pos
|
|
196
|
+
gt = buffer.find(">", pos + prefix_len)
|
|
197
|
+
search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
|
|
198
|
+
return -1
|
|
144
199
|
|
|
145
200
|
|
|
146
|
-
def _actionable_invoke_open_before_close(
|
|
201
|
+
def _actionable_invoke_open_before_close(
|
|
202
|
+
content: str,
|
|
203
|
+
close_pos: int,
|
|
204
|
+
*,
|
|
205
|
+
known_names: Optional[Set[str]] = None,
|
|
206
|
+
) -> bool:
|
|
147
207
|
"""``close_pos`` 处的 ``</entml:invoke>`` 是否闭合带真实 name 的开标签。"""
|
|
148
208
|
search = close_pos - 1
|
|
149
209
|
while search >= 0:
|
|
150
210
|
pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, search + 1)
|
|
151
211
|
if pos < 0:
|
|
152
212
|
return False
|
|
153
|
-
if entml_invoke_open_is_actionable(content, pos):
|
|
213
|
+
if entml_invoke_open_is_actionable(content, pos, known_names=known_names):
|
|
154
214
|
gt = content.find(">", pos)
|
|
155
215
|
if gt < 0 or gt >= close_pos:
|
|
156
216
|
search = pos - 1
|
|
@@ -164,17 +224,26 @@ def _actionable_invoke_open_before_close(content: str, close_pos: int) -> bool:
|
|
|
164
224
|
return False
|
|
165
225
|
|
|
166
226
|
|
|
167
|
-
def _invoke_close_should_strip(
|
|
227
|
+
def _invoke_close_should_strip(
|
|
228
|
+
content: str,
|
|
229
|
+
close_pos: int,
|
|
230
|
+
*,
|
|
231
|
+
known_names: Optional[Set[str]] = None,
|
|
232
|
+
) -> bool:
|
|
168
233
|
"""是否应剥离 ``</entml:invoke>``(孤儿噪声或真实工具块残留)。"""
|
|
169
|
-
if _actionable_invoke_open_before_close(content, close_pos):
|
|
234
|
+
if _actionable_invoke_open_before_close(content, close_pos, known_names=known_names):
|
|
170
235
|
return True
|
|
171
236
|
open_pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, close_pos)
|
|
172
237
|
if open_pos < 0:
|
|
173
238
|
return True
|
|
174
|
-
return entml_invoke_open_is_actionable(content, open_pos)
|
|
239
|
+
return entml_invoke_open_is_actionable(content, open_pos, known_names=known_names)
|
|
175
240
|
|
|
176
241
|
|
|
177
|
-
def iter_actionable_entml_invoke_blocks(
|
|
242
|
+
def iter_actionable_entml_invoke_blocks(
|
|
243
|
+
text: str,
|
|
244
|
+
*,
|
|
245
|
+
known_names: Optional[Set[str]] = None,
|
|
246
|
+
) -> Iterator[Tuple[int, int, str, str]]:
|
|
178
247
|
"""迭代含真实 ``name`` 的完整 ``<entml:invoke>…</entml:invoke>`` 块。"""
|
|
179
248
|
if not text:
|
|
180
249
|
return
|
|
@@ -185,8 +254,9 @@ def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, s
|
|
|
185
254
|
pos = text.find(_INVOKE_OPEN_PREFIX, search_from)
|
|
186
255
|
if pos < 0:
|
|
187
256
|
break
|
|
188
|
-
if not entml_invoke_open_is_actionable(text, pos):
|
|
189
|
-
|
|
257
|
+
if not entml_invoke_open_is_actionable(text, pos, known_names=known_names):
|
|
258
|
+
gt = text.find(">", pos + prefix_len)
|
|
259
|
+
search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
|
|
190
260
|
continue
|
|
191
261
|
gt = text.find(">", pos + prefix_len)
|
|
192
262
|
if gt < 0:
|
|
@@ -200,30 +270,40 @@ def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, s
|
|
|
200
270
|
search_from = close + close_len
|
|
201
271
|
|
|
202
272
|
|
|
203
|
-
def strip_actionable_entml_invoke_blocks(
|
|
273
|
+
def strip_actionable_entml_invoke_blocks(
|
|
274
|
+
text: str,
|
|
275
|
+
*,
|
|
276
|
+
known_names: Optional[Set[str]] = None,
|
|
277
|
+
) -> str:
|
|
204
278
|
"""仅剥离含真实 ``name`` 的 invoke 块;保留 prose ``<entml:invoke>`` 提及。"""
|
|
205
279
|
if not text:
|
|
206
280
|
return text
|
|
207
281
|
parts: List[str] = []
|
|
208
282
|
last = 0
|
|
209
|
-
for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(
|
|
283
|
+
for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(
|
|
284
|
+
text, known_names=known_names
|
|
285
|
+
):
|
|
210
286
|
parts.append(text[last:start])
|
|
211
287
|
last = end
|
|
212
288
|
parts.append(text[last:])
|
|
213
289
|
return "".join(parts)
|
|
214
290
|
|
|
215
291
|
|
|
216
|
-
def _strip_orphan_invoke_tags(
|
|
292
|
+
def _strip_orphan_invoke_tags(
|
|
293
|
+
content: str,
|
|
294
|
+
*,
|
|
295
|
+
known_names: Optional[Set[str]] = None,
|
|
296
|
+
) -> str:
|
|
217
297
|
"""仅剥离带真实 name 的 invoke 孤儿标签;保留 prose 中的 ``<entml:invoke>`` 提及。"""
|
|
218
298
|
pattern = re.compile(r"</?entml:invoke\b[^>]*/?>", re.DOTALL)
|
|
219
299
|
|
|
220
300
|
def repl(match: re.Match[str]) -> str:
|
|
221
301
|
tag = match.group(0)
|
|
222
302
|
if tag.startswith("</"):
|
|
223
|
-
if _invoke_close_should_strip(content, match.start()):
|
|
303
|
+
if _invoke_close_should_strip(content, match.start(), known_names=known_names):
|
|
224
304
|
return ""
|
|
225
305
|
return tag
|
|
226
|
-
if entml_invoke_open_is_actionable(content, match.start()):
|
|
306
|
+
if entml_invoke_open_is_actionable(content, match.start(), known_names=known_names):
|
|
227
307
|
return ""
|
|
228
308
|
return tag
|
|
229
309
|
|
|
@@ -446,13 +526,17 @@ def strip_legacy_function_calls_wrapper(text: str) -> str:
|
|
|
446
526
|
return out
|
|
447
527
|
|
|
448
528
|
|
|
449
|
-
def strip_tool_entml_residue(
|
|
529
|
+
def strip_tool_entml_residue(
|
|
530
|
+
content: str,
|
|
531
|
+
*,
|
|
532
|
+
known_names: Optional[Set[str]] = None,
|
|
533
|
+
) -> str:
|
|
450
534
|
"""剥离工具相关 entml 标签残留,保留 thinking 与非工具 prose 提及。"""
|
|
451
535
|
if not content:
|
|
452
536
|
return content
|
|
453
537
|
cleaned = _TOOL_WRAPPER_PAIR_RE.sub("", content)
|
|
454
|
-
cleaned = strip_actionable_entml_invoke_blocks(cleaned)
|
|
455
|
-
cleaned = _strip_orphan_invoke_tags(cleaned)
|
|
538
|
+
cleaned = strip_actionable_entml_invoke_blocks(cleaned, known_names=known_names)
|
|
539
|
+
cleaned = _strip_orphan_invoke_tags(cleaned, known_names=known_names)
|
|
456
540
|
cleaned = _strip_orphan_non_invoke_tool_tags(cleaned)
|
|
457
541
|
cleaned = _EMPTY_FENCE_RE.sub("", cleaned)
|
|
458
542
|
cleaned = _FENCE_ONLY_LINE_RE.sub("", cleaned)
|
{echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/body.py
RENAMED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
6
6
|
|
|
7
7
|
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
8
8
|
BARE_INVOKE_CHILD_OPEN_RE,
|
|
@@ -59,7 +59,11 @@ def _strip_incomplete_child_close_suffix(value: str, key: str) -> str:
|
|
|
59
59
|
return value
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def split_invoke_open(
|
|
62
|
+
def split_invoke_open(
|
|
63
|
+
buffer: str,
|
|
64
|
+
*,
|
|
65
|
+
known_names: Optional[Set[str]] = None,
|
|
66
|
+
) -> Optional[Tuple[str, int]]:
|
|
63
67
|
"""返回当前正在流式写入的 invoke(最后一个有效开标签)。"""
|
|
64
68
|
search_from = 0
|
|
65
69
|
prefix_len = len(_INVOKE_OPEN_PREFIX)
|
|
@@ -68,6 +72,14 @@ def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
|
|
|
68
72
|
pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
|
|
69
73
|
if pos < 0:
|
|
70
74
|
break
|
|
75
|
+
from echotools.exec.fncall.protocols.entml_patterns import (
|
|
76
|
+
entml_invoke_open_is_actionable,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not entml_invoke_open_is_actionable(buffer, pos, known_names=known_names):
|
|
80
|
+
gt = buffer.find(">", pos + prefix_len)
|
|
81
|
+
search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
|
|
82
|
+
continue
|
|
71
83
|
gt = buffer.find(">", pos + prefix_len)
|
|
72
84
|
if gt < 0:
|
|
73
85
|
break
|