echotools 2.0.0__tar.gz → 2.3.0__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.
Files changed (151) hide show
  1. {echotools-2.0.0 → echotools-2.3.0}/PKG-INFO +1 -1
  2. {echotools-2.0.0 → echotools-2.3.0}/pyproject.toml +1 -1
  3. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/__init__.py +20 -0
  4. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/_version.py +1 -1
  5. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/inject.py +1 -1
  6. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/registry.py +8 -1
  7. echotools-2.3.0/src/echotools/io/__init__.py +37 -0
  8. echotools-2.3.0/src/echotools/io/printstream.py +387 -0
  9. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/protocol/base.py +10 -0
  10. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/__init__.py +5 -0
  11. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/local.py +88 -14
  12. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/session.py +65 -1
  13. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/ssh.py +1 -0
  14. echotools-2.3.0/src/echotools/terminal/tmux.py +105 -0
  15. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/middleware.py +2 -6
  16. {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/PKG-INFO +1 -1
  17. {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/SOURCES.txt +3 -0
  18. {echotools-2.0.0 → echotools-2.3.0}/tests/test_fncall.py +13 -20
  19. echotools-2.3.0/tests/test_printstream.py +339 -0
  20. {echotools-2.0.0 → echotools-2.3.0}/tests/test_protocols.py +6 -6
  21. {echotools-2.0.0 → echotools-2.3.0}/tests/test_web.py +3 -3
  22. echotools-2.0.0/src/echotools/io/__init__.py +0 -11
  23. {echotools-2.0.0 → echotools-2.3.0}/README.md +0 -0
  24. {echotools-2.0.0 → echotools-2.3.0}/setup.cfg +0 -0
  25. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/__init__.py +0 -0
  26. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/list_cache.py +0 -0
  27. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/cache/memory_cache.py +0 -0
  28. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/__init__.py +0 -0
  29. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/base.py +0 -0
  30. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/center.py +0 -0
  31. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/loader.py +0 -0
  32. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/config/merge.py +0 -0
  33. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/__init__.py +0 -0
  34. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/candidate.py +0 -0
  35. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/dispatcher.py +0 -0
  36. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/proxy_selector.py +0 -0
  37. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/selector.py +0 -0
  38. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/dispatch/usage.py +0 -0
  39. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/__init__.py +0 -0
  40. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/base.py +0 -0
  41. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/classify.py +0 -0
  42. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/common.py +0 -0
  43. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/errors/http.py +0 -0
  44. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/__init__.py +0 -0
  45. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/bus.py +0 -0
  46. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/events/event.py +0 -0
  47. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/files/__init__.py +0 -0
  48. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/files/file_util.py +0 -0
  49. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/__init__.py +0 -0
  50. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/__init__.py +0 -0
  51. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/stream.py +0 -0
  52. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/parsers/xml_parser.py +0 -0
  53. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/__init__.py +0 -0
  54. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/history.py +0 -0
  55. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/prompt/templates.py +0 -0
  56. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/protocols/__init__.py +0 -0
  57. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/protocols/entml.py +0 -0
  58. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/__init__.py +0 -0
  59. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/coercion.py +0 -0
  60. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/loop_detect.py +0 -0
  61. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/normalization.py +0 -0
  62. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/fncall/shared/xml_helpers.py +0 -0
  63. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/ids/__init__.py +0 -0
  64. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/ids/generator.py +0 -0
  65. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/io/io_utils.py +0 -0
  66. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/keys.py +0 -0
  67. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/__init__.py +0 -0
  68. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/manager.py +0 -0
  69. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/lifecycle/updater.py +0 -0
  70. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/logger/__init__.py +0 -0
  71. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/logger/manager.py +0 -0
  72. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/network/__init__.py +0 -0
  73. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/network/http_utils.py +0 -0
  74. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/__init__.py +0 -0
  75. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/base.py +0 -0
  76. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/discovery.py +0 -0
  77. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/plugin/registry.py +0 -0
  78. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/process/__init__.py +0 -0
  79. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/process/port.py +0 -0
  80. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/protocol/__init__.py +0 -0
  81. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/proxy/__init__.py +0 -0
  82. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/proxy/manager.py +0 -0
  83. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/py.typed +0 -0
  84. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/retry/__init__.py +0 -0
  85. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/retry/retry.py +0 -0
  86. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/runtime/__init__.py +0 -0
  87. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/runtime/collector.py +0 -0
  88. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/scheduler/__init__.py +0 -0
  89. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/scheduler/scheduler.py +0 -0
  90. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/sdk/__init__.py +0 -0
  91. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/sdk/facade.py +0 -0
  92. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/conpty.py +0 -0
  93. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/terminal/sanitize.py +0 -0
  94. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/__init__.py +0 -0
  95. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/context.py +0 -0
  96. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/span.py +0 -0
  97. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/tracing/tracer.py +0 -0
  98. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/translate.py +0 -0
  99. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/watcher/__init__.py +0 -0
  100. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/watcher/file_watcher.py +0 -0
  101. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/__init__.py +0 -0
  102. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/application.py +0 -0
  103. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/broker.py +0 -0
  104. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/app.js +0 -0
  105. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/file-zone/file-zone.js +0 -0
  106. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-400.woff2 +0 -0
  107. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-500.woff2 +0 -0
  108. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/inter-600.woff2 +0 -0
  109. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/fonts/tailwind.min.css +0 -0
  110. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/input-box.css +0 -0
  111. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/input-box.js +0 -0
  112. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/motion-kit/motion-kit.js +0 -0
  113. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/send-button/send-button.js +0 -0
  114. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/sortable-list/sortable-list.css +0 -0
  115. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/sortable-list/sortable-list.js +0 -0
  116. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/style.css +0 -0
  117. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/text-input/text-input.js +0 -0
  118. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/input_box/voice-input/voice-input.js +0 -0
  119. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/stats.py +0 -0
  120. {echotools-2.0.0 → echotools-2.3.0}/src/echotools/web/utils.py +0 -0
  121. {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/dependency_links.txt +0 -0
  122. {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/requires.txt +0 -0
  123. {echotools-2.0.0 → echotools-2.3.0}/src/echotools.egg-info/top_level.txt +0 -0
  124. {echotools-2.0.0 → echotools-2.3.0}/tests/test_cache_retry.py +0 -0
  125. {echotools-2.0.0 → echotools-2.3.0}/tests/test_completion.py +0 -0
  126. {echotools-2.0.0 → echotools-2.3.0}/tests/test_config.py +0 -0
  127. {echotools-2.0.0 → echotools-2.3.0}/tests/test_config_extended.py +0 -0
  128. {echotools-2.0.0 → echotools-2.3.0}/tests/test_coverage_core.py +0 -0
  129. {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch.py +0 -0
  130. {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch_extended.py +0 -0
  131. {echotools-2.0.0 → echotools-2.3.0}/tests/test_dispatch_race.py +0 -0
  132. {echotools-2.0.0 → echotools-2.3.0}/tests/test_errors.py +0 -0
  133. {echotools-2.0.0 → echotools-2.3.0}/tests/test_errors_common.py +0 -0
  134. {echotools-2.0.0 → echotools-2.3.0}/tests/test_events.py +0 -0
  135. {echotools-2.0.0 → echotools-2.3.0}/tests/test_events_event.py +0 -0
  136. {echotools-2.0.0 → echotools-2.3.0}/tests/test_facade.py +0 -0
  137. {echotools-2.0.0 → echotools-2.3.0}/tests/test_ids.py +0 -0
  138. {echotools-2.0.0 → echotools-2.3.0}/tests/test_imports.py +0 -0
  139. {echotools-2.0.0 → echotools-2.3.0}/tests/test_io.py +0 -0
  140. {echotools-2.0.0 → echotools-2.3.0}/tests/test_keys.py +0 -0
  141. {echotools-2.0.0 → echotools-2.3.0}/tests/test_lifecycle.py +0 -0
  142. {echotools-2.0.0 → echotools-2.3.0}/tests/test_logger.py +0 -0
  143. {echotools-2.0.0 → echotools-2.3.0}/tests/test_merge.py +0 -0
  144. {echotools-2.0.0 → echotools-2.3.0}/tests/test_plugin.py +0 -0
  145. {echotools-2.0.0 → echotools-2.3.0}/tests/test_proxy.py +0 -0
  146. {echotools-2.0.0 → echotools-2.3.0}/tests/test_proxy_selector.py +0 -0
  147. {echotools-2.0.0 → echotools-2.3.0}/tests/test_runtime.py +0 -0
  148. {echotools-2.0.0 → echotools-2.3.0}/tests/test_scheduler.py +0 -0
  149. {echotools-2.0.0 → echotools-2.3.0}/tests/test_tracing.py +0 -0
  150. {echotools-2.0.0 → echotools-2.3.0}/tests/test_usage.py +0 -0
  151. {echotools-2.0.0 → echotools-2.3.0}/tests/test_web_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.0.0
3
+ Version: 2.3.0
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.0.0"
7
+ version = "2.3.0"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -80,6 +80,16 @@ __all__ = [
80
80
  "TerminalCallback",
81
81
  "LocalTerminal",
82
82
  "SSHTerminal",
83
+ "PrintStream",
84
+ "print_stream",
85
+ "start_print_stream",
86
+ "stop_print_stream",
87
+ "flush_print_stream",
88
+ "get_buffer_size",
89
+ "get_queue_length",
90
+ "is_print_stream_running",
91
+ "set_print_speed",
92
+ "configure_print_stream",
83
93
  "inject_fncall",
84
94
  "FncallStreamParser",
85
95
  "detect_tool_loop",
@@ -161,6 +171,16 @@ _EXPORTS: dict[str, tuple[str, str]] = {
161
171
  "TerminalCallback": ("echotools.terminal.session", "TerminalCallback"),
162
172
  "LocalTerminal": ("echotools.terminal.local", "LocalTerminal"),
163
173
  "SSHTerminal": ("echotools.terminal.ssh", "SSHTerminal"),
174
+ "PrintStream": ("echotools.io.printstream", "PrintStream"),
175
+ "print_stream": ("echotools.io.printstream", "print_stream"),
176
+ "start_print_stream": ("echotools.io.printstream", "start_print_stream"),
177
+ "stop_print_stream": ("echotools.io.printstream", "stop_print_stream"),
178
+ "flush_print_stream": ("echotools.io.printstream", "flush_print_stream"),
179
+ "get_buffer_size": ("echotools.io.printstream", "get_buffer_size"),
180
+ "get_queue_length": ("echotools.io.printstream", "get_queue_length"),
181
+ "is_print_stream_running": ("echotools.io.printstream", "is_print_stream_running"),
182
+ "set_print_speed": ("echotools.io.printstream", "set_print_speed"),
183
+ "configure_print_stream": ("echotools.io.printstream", "configure_print_stream"),
164
184
  "inject_fncall": ("echotools.fncall.prompt.inject", "inject_fncall"),
165
185
  "FncallStreamParser": ("echotools.fncall.parsers.stream", "FncallStreamParser"),
166
186
  "detect_tool_loop": ("echotools.fncall.shared.loop_detect", "detect_tool_loop"),
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  """Package version resolution."""
4
4
 
5
- _FALLBACK_VERSION = "1.0.36"
5
+ _FALLBACK_VERSION = "2.3.0"
6
6
 
7
7
 
8
8
  def get_version() -> str:
@@ -71,7 +71,7 @@ def inject_fncall(
71
71
  if loop_detection_threshold > 0:
72
72
  result = detect_tool_loop(normalized, loop_detection_threshold)
73
73
  if result.is_looping:
74
- logger.warning(
74
+ logger.debug(
75
75
  "检测到工具循环(%d 次)", result.repeat_count
76
76
  )
77
77
  loop_warning = result.suggestion
@@ -11,7 +11,7 @@ from echotools.protocol.base import (
11
11
  get_protocol_by_id,
12
12
  )
13
13
 
14
- __all__ = ["get_protocol", "list_protocols"]
14
+ __all__ = ["get_protocol", "list_protocols", "set_custom_protocol_factory", "clear_custom_protocol_factory"]
15
15
 
16
16
  logger = get_logger(__name__)
17
17
 
@@ -29,6 +29,13 @@ def set_custom_protocol_factory(factory) -> None:
29
29
  _custom_factory = factory
30
30
 
31
31
 
32
+ def clear_custom_protocol_factory() -> None:
33
+ """清除 custom 协议工厂与缓存实例(插件 on_unload 时调用)。"""
34
+ global _custom_factory, _custom_instance
35
+ _custom_factory = None
36
+ _custom_instance = None
37
+
38
+
32
39
  def _get_custom_protocol(
33
40
  prompt_en: str = "", prompt_zh: str = ""
34
41
  ) -> ToolProtocol:
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ """io 模块导出。"""
4
+
5
+ from echotools.io.io_utils import (
6
+ atomic_write_text,
7
+ ensure_directory,
8
+ read_text_if_exists,
9
+ )
10
+ from echotools.io.printstream import (
11
+ PrintStream,
12
+ configure_print_stream,
13
+ flush_print_stream,
14
+ get_buffer_size,
15
+ get_queue_length,
16
+ is_print_stream_running,
17
+ print_stream,
18
+ set_print_speed,
19
+ start_print_stream,
20
+ stop_print_stream,
21
+ )
22
+
23
+ __all__ = [
24
+ "atomic_write_text",
25
+ "ensure_directory",
26
+ "read_text_if_exists",
27
+ "PrintStream",
28
+ "print_stream",
29
+ "start_print_stream",
30
+ "stop_print_stream",
31
+ "flush_print_stream",
32
+ "get_buffer_size",
33
+ "get_queue_length",
34
+ "is_print_stream_running",
35
+ "set_print_speed",
36
+ "configure_print_stream",
37
+ ]
@@ -0,0 +1,387 @@
1
+ """Dynamic speed print stream system for controlled console output.
2
+
3
+ This module provides a PrintStream class that implements dynamic speed
4
+ printing with ordered queue management, suitable for controlled output
5
+ of large text blocks with adaptive speed based on queue depth.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import atexit
11
+ import math
12
+ import sys
13
+ import threading
14
+ import time
15
+ from collections import deque
16
+ from typing import Any, Optional
17
+
18
+
19
+ class PrintStream:
20
+ """Dynamic speed print stream system with ordered queue.
21
+
22
+ This class manages a queue of text blocks and outputs them with
23
+ adaptive speed based on the current queue depth. The output speed
24
+ increases as more text is pending, providing smooth, controlled output.
25
+
26
+ Attributes:
27
+ min_speed: Minimum output speed in characters per second.
28
+ max_speed: Maximum output speed in characters per second.
29
+ decay_factor: Controls how quickly speed adapts to queue depth.
30
+ smoothing_factor: Controls speed change smoothing (0.0-1.0).
31
+ current_speed: Current output speed in characters per second.
32
+ accumulated_chars: Fractional characters pending output.
33
+ total_pending_chars: Total characters waiting to be output.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ min_speed: float = 5.0,
39
+ max_speed: float = 100.0,
40
+ decay_factor: float = 20.0,
41
+ smoothing_factor: float = 0.8,
42
+ ) -> None:
43
+ """Initialize the print stream.
44
+
45
+ Args:
46
+ min_speed: Minimum output speed (characters/second).
47
+ max_speed: Maximum output speed (characters/second).
48
+ decay_factor: Controls speed adaptation curve.
49
+ smoothing_factor: Controls speed change smoothing.
50
+ """
51
+ # Queue management
52
+ self._text_queue: deque[str] = deque()
53
+ self._current_text: str = ""
54
+ self._lock = threading.Lock()
55
+
56
+ # Thread management
57
+ self._running = False
58
+ self._started = False
59
+ self._output_thread: Optional[threading.Thread] = None
60
+
61
+ # Speed control parameters
62
+ self.min_speed = max(1.0, min_speed)
63
+ self.max_speed = max(self.min_speed, max_speed)
64
+ self.decay_factor = max(1.0, decay_factor)
65
+ self.smoothing_factor = max(0.1, min(0.99, smoothing_factor))
66
+ self.current_speed = self.min_speed
67
+ self.accumulated_chars = 0.0
68
+
69
+ # Statistics
70
+ self.total_pending_chars = 0
71
+
72
+ def start(self) -> None:
73
+ """Start the print stream system."""
74
+ if not self._running and not self._started:
75
+ self._running = True
76
+ self._started = True
77
+ self._output_thread = threading.Thread(
78
+ target=self._output_processor, daemon=True
79
+ )
80
+ self._output_thread.start()
81
+
82
+ def stop(self) -> None:
83
+ """Stop the print stream system.
84
+
85
+ Waits for all pending output to complete before stopping.
86
+ """
87
+ if self._running:
88
+ self._running = False
89
+ # Wait for all content to be output
90
+ max_wait = 10.0
91
+ start_time = time.time()
92
+ while (self._current_text or self._text_queue) and (
93
+ time.time() - start_time
94
+ ) < max_wait:
95
+ time.sleep(0.1)
96
+
97
+ if self._output_thread and self._output_thread.is_alive():
98
+ self._output_thread.join(timeout=1)
99
+
100
+ def add_to_buffer(self, text: str) -> None:
101
+ """Add text to the output queue.
102
+
103
+ Args:
104
+ text: Text to add to the queue.
105
+ """
106
+ if not self._running:
107
+ self.start()
108
+
109
+ with self._lock:
110
+ self._text_queue.append(str(text))
111
+ self.total_pending_chars += len(str(text))
112
+
113
+ def flush_remaining(self) -> None:
114
+ """Immediately output all remaining content."""
115
+ with self._lock:
116
+ # Output current text being processed
117
+ if self._current_text:
118
+ sys.stdout.write(self._current_text)
119
+ sys.stdout.flush()
120
+ self._current_text = ""
121
+
122
+ # Output all text in queue
123
+ while self._text_queue:
124
+ text = self._text_queue.popleft()
125
+ sys.stdout.write(text)
126
+ sys.stdout.flush()
127
+
128
+ self.total_pending_chars = 0
129
+ self.accumulated_chars = 0.0
130
+
131
+ def _calculate_dynamic_speed(self, buffer_length: int) -> float:
132
+ """Calculate dynamic output speed based on buffer length.
133
+
134
+ Args:
135
+ buffer_length: Number of pending characters.
136
+
137
+ Returns:
138
+ Calculated speed in characters per second.
139
+ """
140
+ if buffer_length <= 0:
141
+ return self.min_speed
142
+
143
+ # Combined exponential and logarithmic function
144
+ exp_component = 1 - math.exp(-buffer_length / self.decay_factor)
145
+ log_component = math.log(1 + buffer_length) / math.log(
146
+ 1 + self.decay_factor
147
+ )
148
+ combined_factor = (
149
+ 2 * exp_component * log_component / (exp_component + log_component + 1e-6)
150
+ )
151
+
152
+ # Calculate target speed
153
+ target_speed = self.min_speed + (self.max_speed - self.min_speed) * combined_factor
154
+
155
+ # Smooth speed changes
156
+ smooth_speed = (
157
+ self.smoothing_factor * self.current_speed
158
+ + (1 - self.smoothing_factor) * target_speed
159
+ )
160
+
161
+ self.current_speed = smooth_speed
162
+ return smooth_speed
163
+
164
+ def _output_processor(self) -> None:
165
+ """Background thread for processing output queue."""
166
+ last_update_time = time.time()
167
+
168
+ while self._running or self._current_text or self._text_queue:
169
+ try:
170
+ current_time = time.time()
171
+ time_delta = current_time - last_update_time
172
+ last_update_time = current_time
173
+
174
+ with self._lock:
175
+ # If no current text, get next from queue
176
+ if not self._current_text and self._text_queue:
177
+ self._current_text = self._text_queue.popleft()
178
+
179
+ # If there's text to output
180
+ if self._current_text:
181
+ # Calculate dynamic speed based on total pending chars
182
+ dynamic_speed = self._calculate_dynamic_speed(
183
+ self.total_pending_chars
184
+ )
185
+
186
+ # Calculate characters to output this iteration
187
+ chars_to_output = dynamic_speed * time_delta + self.accumulated_chars
188
+ actual_chars = int(chars_to_output)
189
+ self.accumulated_chars = chars_to_output - actual_chars
190
+
191
+ # Output characters
192
+ if actual_chars > 0:
193
+ chars_to_print = min(actual_chars, len(self._current_text))
194
+ to_print = self._current_text[:chars_to_print]
195
+ self._current_text = self._current_text[chars_to_print:]
196
+
197
+ # Update total pending chars
198
+ self.total_pending_chars = max(
199
+ 0, self.total_pending_chars - chars_to_print
200
+ )
201
+
202
+ # Output to console
203
+ sys.stdout.write(to_print)
204
+ sys.stdout.flush()
205
+
206
+ # Brief sleep to control update frequency
207
+ time.sleep(0.02) # 50Hz update rate
208
+
209
+ except Exception:
210
+ # Error handling: output remaining content directly
211
+ if self._current_text:
212
+ sys.stdout.write(self._current_text)
213
+ sys.stdout.flush()
214
+ self._current_text = ""
215
+
216
+ @property
217
+ def buffer_size(self) -> int:
218
+ """Get total characters waiting to be output.
219
+
220
+ Returns:
221
+ Number of pending characters.
222
+ """
223
+ with self._lock:
224
+ queue_chars = sum(len(text) for text in self._text_queue)
225
+ return len(self._current_text) + queue_chars
226
+
227
+ @property
228
+ def is_running(self) -> bool:
229
+ """Check if the system is running.
230
+
231
+ Returns:
232
+ True if running, False otherwise.
233
+ """
234
+ return self._running
235
+
236
+ @property
237
+ def queue_length(self) -> int:
238
+ """Get number of text blocks in queue.
239
+
240
+ Returns:
241
+ Number of queued text blocks.
242
+ """
243
+ with self._lock:
244
+ return len(self._text_queue)
245
+
246
+
247
+ # Global instance
248
+ _global_print_stream = PrintStream()
249
+
250
+
251
+ def print_stream(
252
+ *args: Any,
253
+ sep: str = " ",
254
+ end: str = "\n",
255
+ flush: bool = False,
256
+ ) -> None:
257
+ """Dynamic speed print function.
258
+
259
+ This function provides a drop-in replacement for the built-in print()
260
+ function with dynamic speed output based on queue depth.
261
+
262
+ Args:
263
+ *args: Content to print.
264
+ sep: Separator between arguments (default: space).
265
+ end: End character (default: newline).
266
+ flush: If True, output immediately without queuing.
267
+ """
268
+ try:
269
+ # Ensure system is started
270
+ if not _global_print_stream.is_running:
271
+ _global_print_stream.start()
272
+
273
+ # Combine output content
274
+ text = sep.join(str(arg) for arg in args) + end
275
+
276
+ if flush:
277
+ # Output immediately
278
+ sys.stdout.write(text)
279
+ sys.stdout.flush()
280
+ else:
281
+ # Add to queue
282
+ _global_print_stream.add_to_buffer(text)
283
+
284
+ except Exception:
285
+ # Fallback to standard print on error
286
+ print(*args, sep=sep, end=end)
287
+
288
+
289
+ def start_print_stream() -> None:
290
+ """Manually start the print stream system."""
291
+ _global_print_stream.start()
292
+
293
+
294
+ def stop_print_stream() -> None:
295
+ """Stop the print stream system."""
296
+ _global_print_stream.stop()
297
+
298
+
299
+ def flush_print_stream() -> None:
300
+ """Immediately output all buffered content."""
301
+ _global_print_stream.flush_remaining()
302
+
303
+
304
+ def get_buffer_size() -> int:
305
+ """Get current buffer size.
306
+
307
+ Returns:
308
+ Number of pending characters.
309
+ """
310
+ return _global_print_stream.buffer_size
311
+
312
+
313
+ def get_queue_length() -> int:
314
+ """Get number of text blocks in queue.
315
+
316
+ Returns:
317
+ Number of queued text blocks.
318
+ """
319
+ return _global_print_stream.queue_length
320
+
321
+
322
+ def is_print_stream_running() -> bool:
323
+ """Check if the print stream system is running.
324
+
325
+ Returns:
326
+ True if running, False otherwise.
327
+ """
328
+ return _global_print_stream.is_running
329
+
330
+
331
+ def set_print_speed(min_speed: float = 5.0, max_speed: float = 50.0) -> None:
332
+ """Set print speed range.
333
+
334
+ Args:
335
+ min_speed: Minimum print speed (characters/second).
336
+ max_speed: Maximum print speed (characters/second).
337
+ """
338
+ _global_print_stream.min_speed = max(1.0, min_speed)
339
+ _global_print_stream.max_speed = max(_global_print_stream.min_speed, max_speed)
340
+
341
+
342
+ def configure_print_stream(
343
+ min_speed: float = 5.0,
344
+ max_speed: float = 50.0,
345
+ decay_factor: float = 20.0,
346
+ smoothing_factor: float = 0.8,
347
+ ) -> None:
348
+ """Configure print stream system parameters.
349
+
350
+ Args:
351
+ min_speed: Minimum print speed.
352
+ max_speed: Maximum print speed.
353
+ decay_factor: Decay factor for speed adaptation.
354
+ smoothing_factor: Smoothing factor for speed changes.
355
+ """
356
+ _global_print_stream.min_speed = max(1.0, min_speed)
357
+ _global_print_stream.max_speed = max(_global_print_stream.min_speed, max_speed)
358
+ _global_print_stream.decay_factor = max(1.0, decay_factor)
359
+ _global_print_stream.smoothing_factor = max(0.1, min(0.99, smoothing_factor))
360
+
361
+
362
+ def _cleanup() -> None:
363
+ """Cleanup function registered with atexit."""
364
+ try:
365
+ _global_print_stream.flush_remaining()
366
+ _global_print_stream.stop()
367
+ except Exception:
368
+ pass
369
+
370
+
371
+ # Register cleanup function
372
+ atexit.register(_cleanup)
373
+
374
+
375
+ # Module exports
376
+ __all__ = [
377
+ "PrintStream",
378
+ "print_stream",
379
+ "start_print_stream",
380
+ "stop_print_stream",
381
+ "flush_print_stream",
382
+ "get_buffer_size",
383
+ "get_queue_length",
384
+ "is_print_stream_running",
385
+ "set_print_speed",
386
+ "configure_print_stream",
387
+ ]
@@ -10,6 +10,7 @@ from echotools.logger.manager import get_logger
10
10
  __all__ = [
11
11
  "ToolProtocol",
12
12
  "register_protocol",
13
+ "unregister_protocol",
13
14
  "get_protocol_by_id",
14
15
  "list_protocols",
15
16
  "VALID_PROTOCOL_IDS",
@@ -104,6 +105,15 @@ def register_protocol(protocol: ToolProtocol) -> None:
104
105
  logger.debug("已注册协议: %s", protocol.id)
105
106
 
106
107
 
108
+ def unregister_protocol(protocol_id: str) -> bool:
109
+ """注销协议实现;内置 entml 等 Host 保留协议不应移除。"""
110
+ if protocol_id not in _PROTOCOL_REGISTRY:
111
+ return False
112
+ del _PROTOCOL_REGISTRY[protocol_id]
113
+ logger.debug("已注销协议: %s", protocol_id)
114
+ return True
115
+
116
+
107
117
  def get_protocol_by_id(protocol_id: str) -> ToolProtocol:
108
118
  """按 ID 获取协议。"""
109
119
  if protocol_id not in _PROTOCOL_REGISTRY:
@@ -14,17 +14,22 @@ from .sanitize import sanitize_terminal_history_chunk
14
14
  from .session import (
15
15
  MAX_HISTORY_LINES,
16
16
  MAX_OFFLINE_BUFFER_BYTES,
17
+ MAX_SEQ_RING_BYTES,
17
18
  TerminalCallback,
18
19
  TerminalSession,
19
20
  )
20
21
  from .ssh import SSHTerminal
22
+ from .tmux import TmuxTerminal, tmux_available
21
23
 
22
24
  __all__ = [
23
25
  "TerminalSession",
24
26
  "TerminalCallback",
25
27
  "LocalTerminal",
26
28
  "SSHTerminal",
29
+ "TmuxTerminal",
30
+ "tmux_available",
27
31
  "MAX_OFFLINE_BUFFER_BYTES",
32
+ "MAX_SEQ_RING_BYTES",
28
33
  "MAX_HISTORY_LINES",
29
34
  "sanitize_terminal_history_chunk",
30
35
  ]