cellium-agent 1.4.0__py3-none-any.whl

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 (224) hide show
  1. app/__init__.py +4 -0
  2. app/agent/control/__init__.py +84 -0
  3. app/agent/control/action_bandit.py +336 -0
  4. app/agent/control/constraint_gene/__init__.py +15 -0
  5. app/agent/control/constraint_gene/composer.py +158 -0
  6. app/agent/control/constraint_gene/evolution.py +954 -0
  7. app/agent/control/constraint_gene/matcher.py +241 -0
  8. app/agent/control/control_loop.py +590 -0
  9. app/agent/control/decision_renderer.py +184 -0
  10. app/agent/control/feedback_evaluator.py +276 -0
  11. app/agent/control/gene_post_session.py +200 -0
  12. app/agent/control/hard_constraints.py +459 -0
  13. app/agent/control/hybrid_controller.py +440 -0
  14. app/agent/control/loop_state.py +179 -0
  15. app/agent/control/thought_parser.py +174 -0
  16. app/agent/di_config.py +568 -0
  17. app/agent/events/__init__.py +40 -0
  18. app/agent/events/event_models.py +115 -0
  19. app/agent/events/event_types.py +37 -0
  20. app/agent/heuristics/__init__.py +42 -0
  21. app/agent/heuristics/config.py +108 -0
  22. app/agent/heuristics/engine.py +373 -0
  23. app/agent/heuristics/features.py +588 -0
  24. app/agent/heuristics/integration.py +237 -0
  25. app/agent/heuristics/rules/__init__.py +30 -0
  26. app/agent/heuristics/rules/base_rule.py +60 -0
  27. app/agent/heuristics/rules/loop_detection.py +242 -0
  28. app/agent/heuristics/rules/termination.py +404 -0
  29. app/agent/heuristics/scoring.py +175 -0
  30. app/agent/heuristics/trace.py +135 -0
  31. app/agent/heuristics/types.py +134 -0
  32. app/agent/learning/__init__.py +24 -0
  33. app/agent/learning/bandit.py +97 -0
  34. app/agent/learning/integration.py +282 -0
  35. app/agent/learning/memory_policy.py +184 -0
  36. app/agent/learning/policy.py +143 -0
  37. app/agent/llm/__init__.py +10 -0
  38. app/agent/llm/engine.py +958 -0
  39. app/agent/llm/models.py +31 -0
  40. app/agent/llm/transport.py +259 -0
  41. app/agent/loop/__init__.py +29 -0
  42. app/agent/loop/agent_loop.py +2303 -0
  43. app/agent/loop/agent_loop_manager.py +289 -0
  44. app/agent/loop/auto_hints.py +438 -0
  45. app/agent/loop/command_handler.py +74 -0
  46. app/agent/loop/loop_controller.py +122 -0
  47. app/agent/loop/loop_event_publisher.py +268 -0
  48. app/agent/loop/memory.py +347 -0
  49. app/agent/loop/round_trimmer.py +75 -0
  50. app/agent/loop/session_manager.py +444 -0
  51. app/agent/loop/session_store.py +329 -0
  52. app/agent/loop/tool_executor.py +671 -0
  53. app/agent/memory/__init__.py +30 -0
  54. app/agent/memory/archive_store.py +106 -0
  55. app/agent/memory/chinese_tokenizer.py +112 -0
  56. app/agent/memory/fts5_searcher.py +699 -0
  57. app/agent/memory/knowledge_extractor.py +189 -0
  58. app/agent/memory/repository.py +1521 -0
  59. app/agent/memory/session_compact.py +631 -0
  60. app/agent/memory/session_notes.py +323 -0
  61. app/agent/memory/three_layer.py +319 -0
  62. app/agent/memory/vector_native.py +169 -0
  63. app/agent/prompt/__init__.py +21 -0
  64. app/agent/prompt/builder.py +170 -0
  65. app/agent/prompt/diff.py +247 -0
  66. app/agent/prompt/piece.py +96 -0
  67. app/agent/prompt/pieces.py +304 -0
  68. app/agent/runtime/__init__.py +14 -0
  69. app/agent/runtime/context.py +841 -0
  70. app/agent/runtime/core.py +314 -0
  71. app/agent/runtime/diagnostics.py +375 -0
  72. app/agent/runtime/patch.py +126 -0
  73. app/agent/runtime/patch_applier.py +301 -0
  74. app/agent/runtime/transaction.py +78 -0
  75. app/agent/security/__init__.py +13 -0
  76. app/agent/security/policy.py +11 -0
  77. app/agent/shell/__init__.py +3 -0
  78. app/agent/shell/cellium_shell.py +1193 -0
  79. app/agent/tools/__init__.py +13 -0
  80. app/agent/tools/base_tool.py +338 -0
  81. app/agent/tools/config_tool.py +454 -0
  82. app/agent/tools/edit_tool.py +299 -0
  83. app/agent/tools/file_cache.py +100 -0
  84. app/agent/tools/file_tool.py +315 -0
  85. app/agent/tools/glob_tool.py +86 -0
  86. app/agent/tools/grep_tool.py +606 -0
  87. app/agent/tools/ls_tool.py +97 -0
  88. app/agent/tools/memory_tool.py +823 -0
  89. app/agent/tools/read_tool.py +453 -0
  90. app/agent/tools/shell_tool.py +345 -0
  91. app/channels/__init__.py +99 -0
  92. app/channels/base.py +289 -0
  93. app/channels/channel_manager.py +673 -0
  94. app/channels/channel_registry.py +44 -0
  95. app/channels/feishu/__init__.py +5 -0
  96. app/channels/feishu/feishu_adapter.py +852 -0
  97. app/channels/feishu/feishu_config.py +89 -0
  98. app/channels/qq/__init__.py +6 -0
  99. app/channels/qq/qq_adapter.py +1151 -0
  100. app/channels/qq/qq_config.py +78 -0
  101. app/channels/qq/qq_connect_client.py +284 -0
  102. app/channels/telegram/__init__.py +5 -0
  103. app/channels/telegram/telegram_adapter.py +1022 -0
  104. app/channels/telegram/telegram_config.py +103 -0
  105. app/channels/weixin/__init__.py +5 -0
  106. app/channels/weixin/weixin_adapter.py +1383 -0
  107. app/channels/weixin/weixin_config.py +68 -0
  108. app/core/__init__.py +10 -0
  109. app/core/bootstrap.py +211 -0
  110. app/core/bus/__init__.py +51 -0
  111. app/core/bus/event_bus.py +531 -0
  112. app/core/bus/event_models.py +194 -0
  113. app/core/bus/events.py +28 -0
  114. app/core/di/__init__.py +17 -0
  115. app/core/di/container.py +180 -0
  116. app/core/exception.py +16 -0
  117. app/core/interface/base_cell.py +94 -0
  118. app/core/interface/icell.py +68 -0
  119. app/core/interface/memory.py +45 -0
  120. app/core/scheduler/__init__.py +30 -0
  121. app/core/scheduler/executor.py +297 -0
  122. app/core/scheduler/manager.py +417 -0
  123. app/core/security/__init__.py +2 -0
  124. app/core/security/policy.py +390 -0
  125. app/core/util/__init__.py +16 -0
  126. app/core/util/agent_config.py +642 -0
  127. app/core/util/browser_runtime.py +67 -0
  128. app/core/util/browser_utils.py +163 -0
  129. app/core/util/cell_tool_adapter.py +872 -0
  130. app/core/util/component_auditor.py +540 -0
  131. app/core/util/component_sandbox.py +592 -0
  132. app/core/util/component_tool_registry.py +330 -0
  133. app/core/util/component_watcher.py +334 -0
  134. app/core/util/components_loader.py +1037 -0
  135. app/core/util/logger.py +581 -0
  136. app/core/util/mp_manager.py +183 -0
  137. app/core/util/protected_modules.py +263 -0
  138. app/core/util/runtime_paths.py +236 -0
  139. app/core/util/sandbox_entry.py +228 -0
  140. app/core/window/__init__.py +3 -0
  141. app/core/window/main_window.py +665 -0
  142. app/server/__init__.py +24 -0
  143. app/server/routes/__init__.py +2 -0
  144. app/server/routes/channels.py +306 -0
  145. app/server/routes/chat.py +997 -0
  146. app/server/routes/components.py +275 -0
  147. app/server/routes/config.py +497 -0
  148. app/server/routes/gene.py +299 -0
  149. app/server/routes/logs.py +171 -0
  150. app/server/routes/memory.py +225 -0
  151. app/server/routes/scheduler.py +209 -0
  152. app/server/routes/session_events.py +120 -0
  153. app/server/routes/skills.py +151 -0
  154. app/server/routes/upload.py +158 -0
  155. app/server/routes/ws_event_manager.py +322 -0
  156. app/server/task_manager.py +440 -0
  157. app/server/web_server.py +147 -0
  158. app/tui/__init__.py +2 -0
  159. app/tui/app.py +1799 -0
  160. app/tui/commands.py +45 -0
  161. app/tui/history_render.py +285 -0
  162. app/tui/i18n.py +423 -0
  163. app/tui/model_picker.py +315 -0
  164. app/tui/runner.py +151 -0
  165. app/tui/settings_screen.py +213 -0
  166. app/tui/spinner.py +20 -0
  167. app/tui/theme.py +112 -0
  168. app/tui/widgets.py +1148 -0
  169. cellium_agent-1.4.0.data/data/cellium/components/__init__.py +8 -0
  170. cellium_agent-1.4.0.data/data/cellium/components/_example_component.py +299 -0
  171. cellium_agent-1.4.0.data/data/cellium/components/component_builder.py +1247 -0
  172. cellium_agent-1.4.0.data/data/cellium/components/feishu_files.py +466 -0
  173. cellium_agent-1.4.0.data/data/cellium/components/qq_files.py +340 -0
  174. cellium_agent-1.4.0.data/data/cellium/components/scheduler.py +256 -0
  175. cellium_agent-1.4.0.data/data/cellium/components/skill_installer.py +979 -0
  176. cellium_agent-1.4.0.data/data/cellium/components/skill_manager.py +222 -0
  177. cellium_agent-1.4.0.data/data/cellium/components/skills/__init__.py +39 -0
  178. cellium_agent-1.4.0.data/data/cellium/components/sub_agent.py +625 -0
  179. cellium_agent-1.4.0.data/data/cellium/components/telegram_files.py +353 -0
  180. cellium_agent-1.4.0.data/data/cellium/components/web_fetch.py +2563 -0
  181. cellium_agent-1.4.0.data/data/cellium/components/web_search.py +1076 -0
  182. cellium_agent-1.4.0.data/data/cellium/components/weixin_files.py +342 -0
  183. cellium_agent-1.4.0.data/data/cellium/config/agent/agent.yaml +9 -0
  184. cellium_agent-1.4.0.data/data/cellium/config/agent/channels.yaml +24 -0
  185. cellium_agent-1.4.0.data/data/cellium/config/agent/heuristics.yaml +43 -0
  186. cellium_agent-1.4.0.data/data/cellium/config/agent/learning.yaml +30 -0
  187. cellium_agent-1.4.0.data/data/cellium/config/agent/llm.yaml +18 -0
  188. cellium_agent-1.4.0.data/data/cellium/config/agent/logging.yaml +11 -0
  189. cellium_agent-1.4.0.data/data/cellium/config/agent/memory.yaml +22 -0
  190. cellium_agent-1.4.0.data/data/cellium/config/agent/model_registry.yaml +105 -0
  191. cellium_agent-1.4.0.data/data/cellium/config/agent/routes.yaml +26 -0
  192. cellium_agent-1.4.0.data/data/cellium/config/agent/security.yaml +20 -0
  193. cellium_agent-1.4.0.data/data/cellium/config/agent/server.yaml +18 -0
  194. cellium_agent-1.4.0.data/data/cellium/config/settings.yaml +24 -0
  195. cellium_agent-1.4.0.data/data/cellium/dll/.gitkeep +3 -0
  196. cellium_agent-1.4.0.data/data/cellium/dll/darwin-arm64/libvector_engine.dylib +0 -0
  197. cellium_agent-1.4.0.data/data/cellium/dll/linux-aarch64/libvector_engine.so +0 -0
  198. cellium_agent-1.4.0.data/data/cellium/dll/linux-x86_64/libvector_engine.so +0 -0
  199. cellium_agent-1.4.0.data/data/cellium/dll/windows-x86_64/vector_engine.dll +0 -0
  200. cellium_agent-1.4.0.data/data/cellium/html/assets/SettingsPage-D_NlgEkQ.js +1 -0
  201. cellium_agent-1.4.0.data/data/cellium/html/assets/i18n-vendor-CLSNeGwf.js +1 -0
  202. cellium_agent-1.4.0.data/data/cellium/html/assets/index-BTZUwOV4.css +1 -0
  203. cellium_agent-1.4.0.data/data/cellium/html/assets/index-CfsVsH8O.js +4 -0
  204. cellium_agent-1.4.0.data/data/cellium/html/assets/markdown-vendor-Ckj0zzMC.js +66 -0
  205. cellium_agent-1.4.0.data/data/cellium/html/assets/react-vendor-CEaRprYI.js +23 -0
  206. cellium_agent-1.4.0.data/data/cellium/html/assets/rolldown-runtime-xSXa1GVp.js +1 -0
  207. cellium_agent-1.4.0.data/data/cellium/html/font/Roboto-Regular.ttf +0 -0
  208. cellium_agent-1.4.0.data/data/cellium/html/index.html +27 -0
  209. cellium_agent-1.4.0.data/data/cellium/html/logo.png +0 -0
  210. cellium_agent-1.4.0.data/data/cellium/licenses/ripgrep-LICENSE +26 -0
  211. cellium_agent-1.4.0.data/data/cellium/memory/personality.md +385 -0
  212. cellium_agent-1.4.0.data/data/cellium/vendor/.gitkeep +0 -0
  213. cellium_agent-1.4.0.data/data/cellium/vendor/ripgrep/.gitkeep +0 -0
  214. cellium_agent-1.4.0.data/data/cellium/vendor/ripgrep/aarch64-apple-darwin/rg +0 -0
  215. cellium_agent-1.4.0.data/data/cellium/vendor/ripgrep/aarch64-unknown-linux-gnu/rg +0 -0
  216. cellium_agent-1.4.0.data/data/cellium/vendor/ripgrep/x86_64-pc-windows-msvc/rg.exe +0 -0
  217. cellium_agent-1.4.0.data/data/cellium/vendor/ripgrep/x86_64-unknown-linux-musl/rg +0 -0
  218. cellium_agent-1.4.0.dist-info/METADATA +725 -0
  219. cellium_agent-1.4.0.dist-info/RECORD +224 -0
  220. cellium_agent-1.4.0.dist-info/WHEEL +5 -0
  221. cellium_agent-1.4.0.dist-info/entry_points.txt +3 -0
  222. cellium_agent-1.4.0.dist-info/licenses/LICENSE +13 -0
  223. cellium_agent-1.4.0.dist-info/top_level.txt +1 -0
  224. main.py +63 -0
app/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Cellium Agent - 应用根包
4
+ """
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Control Loop Harness - LLM 控制环
4
+
5
+ 核心组件:
6
+ - LoopState: 控制环状态快照
7
+ - ControlDecision: Action-based 决策
8
+ - FeedbackEvaluator: 分段式反馈评估
9
+ - ActionBandit: Action 选择器
10
+ - ControlLoop: 统一控制入口
11
+ - DecisionRenderer: 软约束渲染器(默认)
12
+ - HardConstraintRenderer: 强约束渲染器(PromptBuilder v3)
13
+ - ThoughtParser: 思考解析器
14
+ - HybridController: Plan-Execute-Observe-RePlan 混合控制器
15
+
16
+ Gene 相关(constraint_gene 子模块):
17
+ - TaskSignalMatcher: 任务信号匹配器
18
+ - GeneEvolution: Gene 进化系统
19
+ - GeneComposer: Gene 组合器
20
+ """
21
+
22
+ from .loop_state import LoopState, ControlDecision
23
+ from .feedback_evaluator import FeedbackEvaluator
24
+ from .action_bandit import ActionBandit
25
+ from .control_loop import ControlLoop, create_control_loop
26
+ from .decision_renderer import DecisionRenderer, RenderedPrompt
27
+ from .hard_constraints import (
28
+ HardConstraint,
29
+ HardConstraintRenderer,
30
+ HardConstraintTemplates,
31
+ ActionFusion,
32
+ )
33
+ from .thought_parser import (
34
+ ThoughtParser,
35
+ ParsedThought,
36
+ ThoughtStep,
37
+ ActionType,
38
+ THOUGHT_SCHEMA,
39
+ )
40
+ from .hybrid_controller import (
41
+ HybridController,
42
+ HybridPhase,
43
+ HybridState,
44
+ Observation,
45
+ create_hybrid_controller,
46
+ )
47
+
48
+ from .constraint_gene import TaskSignalMatcher, GeneEvolution, GeneComposer
49
+ from .gene_post_session import GenePostSessionAnalyzer, analyze_session_for_gene
50
+
51
+ __all__ = [
52
+ "LoopState",
53
+ "ControlDecision",
54
+ "FeedbackEvaluator",
55
+ "ActionBandit",
56
+ "ControlLoop",
57
+ "create_control_loop",
58
+ "DecisionRenderer",
59
+ "RenderedPrompt",
60
+ # 强约束版
61
+ "HardConstraint",
62
+ "HardConstraintRenderer",
63
+ "HardConstraintTemplates",
64
+ "ActionFusion",
65
+ # 思考系统
66
+ "ThoughtParser",
67
+ "ParsedThought",
68
+ "ThoughtStep",
69
+ "ActionType",
70
+ "THOUGHT_SCHEMA",
71
+ # Hybrid 控制器
72
+ "HybridController",
73
+ "HybridPhase",
74
+ "HybridState",
75
+ "Observation",
76
+ "create_hybrid_controller",
77
+ # Gene 相关
78
+ "TaskSignalMatcher",
79
+ "GeneEvolution",
80
+ "GeneComposer",
81
+ # 会话后 Gene 分析
82
+ "GenePostSessionAnalyzer",
83
+ "analyze_session_for_gene",
84
+ ]
@@ -0,0 +1,336 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ActionBandit - Action-based Bandit
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import os
9
+ from collections import deque
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ ACTION_TYPES = ["continue", "retry", "redirect", "compress", "terminate"]
17
+ ACTION_PRIORITY = ["continue", "retry", "compress", "redirect", "terminate"]
18
+
19
+
20
+ @dataclass
21
+ class ActionStats:
22
+ """单个 Action 的统计数据"""
23
+ alpha: float = 2.0
24
+ beta: float = 2.0
25
+ count: int = 0
26
+
27
+ def to_dict(self) -> Dict:
28
+ return {"alpha": self.alpha, "beta": self.beta, "count": self.count}
29
+
30
+ @classmethod
31
+ def from_dict(cls, data: Dict) -> "ActionStats":
32
+ return cls(
33
+ alpha=data.get("alpha", 2.0),
34
+ beta=data.get("beta", 2.0),
35
+ count=data.get("count", 0),
36
+ )
37
+
38
+
39
+ class ActionBandit:
40
+ """
41
+ Action-based Bandit
42
+ """
43
+
44
+ def __init__(self, memory_path: Optional[str] = None, n_step: int = 3):
45
+ """
46
+ 初始化
47
+
48
+ Args:
49
+ memory_path: 统计数据持久化路径
50
+ n_step: n-step return 窗口大小
51
+ """
52
+ self.memory_path = memory_path
53
+ self.n_step = n_step
54
+
55
+ self._stats: Dict[str, ActionStats] = {
56
+ action: ActionStats() for action in ACTION_TYPES
57
+ }
58
+
59
+ self._decay_factor = 0.99
60
+ self._decay_interval = 50
61
+ self._session_count = 0
62
+
63
+ self._policy_thresholds: Dict[str, Any] = {}
64
+ self._reward_buffer: deque = deque(maxlen=n_step)
65
+ self._last_actions: deque = deque(maxlen=n_step)
66
+
67
+ if memory_path:
68
+ self._load()
69
+
70
+ def select_action(
71
+ self,
72
+ features: Any,
73
+ candidate_actions: Optional[List[str]] = None,
74
+ ) -> str:
75
+ """
76
+ 选择 Action。
77
+
78
+ 当前策略:
79
+ 1. 规则/控制环先给出候选 action
80
+ 2. Bandit 只在候选集合内部做 tie-break
81
+ 3. terminate 仍保留硬规则保护
82
+ """
83
+ is_terminate_hard = (
84
+ hasattr(features, 'is_output_loop')
85
+ and features.is_output_loop
86
+ and hasattr(features, 'exact_repetition_count')
87
+ and features.exact_repetition_count >= 5
88
+ )
89
+
90
+ if is_terminate_hard:
91
+ logger.info(
92
+ "[ActionBandit] 硬规则: terminate (output_loop=%d)",
93
+ features.exact_repetition_count
94
+ )
95
+ return "terminate"
96
+
97
+ candidates = self._normalize_candidates(candidate_actions)
98
+ if not candidates:
99
+ return "continue"
100
+
101
+ if len(candidates) == 1:
102
+ return candidates[0]
103
+
104
+ action = self._thompson_with_bias(features, candidate_actions=candidates)
105
+ logger.debug("[ActionBandit] tie-break: %s | candidates=%s", action, candidates)
106
+ return action
107
+
108
+ def _normalize_candidates(self, candidate_actions: Optional[List[str]]) -> List[str]:
109
+ if not candidate_actions:
110
+ return [action for action in ACTION_TYPES if action != "terminate"]
111
+
112
+ candidates = []
113
+ for action in candidate_actions:
114
+ if action in ACTION_TYPES and action not in candidates:
115
+ candidates.append(action)
116
+
117
+ return candidates
118
+
119
+ def _thompson_with_bias(self, features: Any, candidate_actions: Optional[List[str]] = None) -> str:
120
+ """
121
+ Thompson Sampling + Heuristic Bias
122
+
123
+ 原理:
124
+ - 每个 action 从 Beta 分布采样
125
+ - 加上 Heuristic 提供的 bias
126
+ - 选择分数最高的 action
127
+ """
128
+ import random
129
+
130
+ candidates = self._normalize_candidates(candidate_actions) or list(self._stats.keys())
131
+
132
+ scores = {}
133
+ for action in candidates:
134
+ stats = self._stats[action]
135
+
136
+ try:
137
+ import numpy as np
138
+ sample = np.random.beta(stats.alpha, stats.beta)
139
+ except ImportError:
140
+ mean = stats.alpha / (stats.alpha + stats.beta) if (stats.alpha + stats.beta) > 0 else 0.5
141
+ noise = random.gauss(0, 0.1)
142
+ sample = max(0, min(1, mean + noise))
143
+
144
+ bias = self._heuristic_bias(action, features)
145
+
146
+ scores[action] = sample + bias
147
+
148
+ if not scores:
149
+ return "continue"
150
+
151
+ max_score = max(scores.values())
152
+ best_actions = [a for a, s in scores.items() if s == max_score]
153
+
154
+ # 优先级:continue > retry > compress > redirect > terminate
155
+ for priority_action in ACTION_PRIORITY:
156
+ if priority_action in best_actions:
157
+ return priority_action
158
+
159
+ return best_actions[0]
160
+
161
+ def _heuristic_bias(self, action: str, features: Any) -> float:
162
+ """
163
+ Heuristic Bias 计算
164
+
165
+ 原理:
166
+ - Heuristic 不决定 action,只调整分数
167
+ - bias > 0 表示"推荐",但 Bandit 仍可选择其他
168
+ - 不同 action 有不同的触发条件
169
+ - 使用 Policy 阈值动态调整判断条件
170
+ - 冷启动保护:统计不足时自动放大 bias,让启发式兜底
171
+
172
+ Returns:
173
+ bias 值 [0, 1]
174
+ """
175
+ bias = 0.0
176
+
177
+ stuck_threshold = self._policy_thresholds.get("stuck_iterations", 5)
178
+ repetition_threshold = self._policy_thresholds.get("repetition_threshold", 4)
179
+ repetition_score_threshold = self._policy_thresholds.get("repetition_score", 0.7)
180
+
181
+ if action == "redirect":
182
+ if hasattr(features, 'repetition_score') and features.repetition_score > repetition_score_threshold:
183
+ bias = max(bias, 0.03 * features.repetition_score)
184
+ if hasattr(features, 'stuck_iterations') and features.stuck_iterations >= stuck_threshold:
185
+ bias = max(bias, 0.04)
186
+
187
+ if action == "retry":
188
+ if hasattr(features, 'stuck_iterations') and 1 <= features.stuck_iterations < stuck_threshold:
189
+ bias = max(bias, 0.02)
190
+ if hasattr(features, 'progress_trend') and 0 < features.progress_trend < 0.3:
191
+ bias = max(bias, 0.01)
192
+
193
+ if action == "compress":
194
+ if hasattr(features, 'context_saturation') and features.context_saturation > 0.6:
195
+ bias = max(bias, 0.03)
196
+ if hasattr(features, 'stuck_iterations') and features.stuck_iterations >= stuck_threshold // 2:
197
+ bias = max(bias, 0.02)
198
+
199
+ if action == "continue":
200
+ if hasattr(features, 'progress_score') and features.progress_score > 0.5:
201
+ bias = max(bias, 0.02)
202
+ if hasattr(features, 'stuck_iterations') and features.stuck_iterations == 0:
203
+ bias = max(bias, 0.03)
204
+
205
+ # 冷启动保护:该 action 统计不足时放大 bias,退回启发式主导
206
+ if action in self._stats and self._stats[action].count < 5:
207
+ bias *= 5
208
+
209
+ return bias
210
+
211
+ def set_policy_thresholds(self, thresholds: Dict[str, Any]):
212
+ """设置 Policy 阈值约束"""
213
+ self._policy_thresholds = thresholds
214
+ logger.info("[ActionBandit] Policy 阈值已设置: %s", list(thresholds.keys()))
215
+
216
+ def update(self, action: str, reward: float):
217
+ if action not in self._stats:
218
+ logger.warning("[ActionBandit] 未知 action: %s", action)
219
+ return
220
+
221
+ self._reward_buffer.append(reward)
222
+ self._last_actions.append(action)
223
+
224
+ n_step_reward = self._compute_n_step_return()
225
+
226
+ stats = self._stats[action]
227
+ stats.count += 1
228
+
229
+ if n_step_reward > 0.5:
230
+ stats.alpha += n_step_reward
231
+ else:
232
+ stats.beta += (1 - n_step_reward)
233
+
234
+ logger.debug(
235
+ "[ActionBandit] 更新 %s | reward=%.2f | n_step=%.2f | alpha=%.1f beta=%.1f",
236
+ action, reward, n_step_reward, stats.alpha, stats.beta
237
+ )
238
+
239
+ if self.memory_path:
240
+ self._save()
241
+
242
+ def _compute_n_step_return(self) -> float:
243
+ """
244
+ 计算 n-step return
245
+ G_t = (r_t + r_{t+1} + ... + r_{t+n-1}) / n
246
+
247
+ """
248
+ if not self._reward_buffer:
249
+ return 0.5 # 默认中性
250
+
251
+ return sum(self._reward_buffer) / len(self._reward_buffer)
252
+
253
+ def end_session(self):
254
+ self._session_count += 1
255
+
256
+ if self._session_count >= self._decay_interval:
257
+ self._decay()
258
+ self._session_count = 0
259
+
260
+ self._reward_buffer.clear()
261
+ self._last_actions.clear()
262
+
263
+ def _decay(self):
264
+ for action, stats in self._stats.items():
265
+ stats.alpha = 1 + (stats.alpha - 1) * self._decay_factor
266
+ stats.beta = 1 + (stats.beta - 1) * self._decay_factor
267
+
268
+ logger.info("[ActionBandit] 统计已衰减 (factor=%.2f)", self._decay_factor)
269
+
270
+ def get_stats(self) -> Dict[str, Dict]:
271
+ return {
272
+ action: stats.to_dict()
273
+ for action, stats in self._stats.items()
274
+ }
275
+
276
+ def get_summary(self) -> Dict[str, Any]:
277
+ summary = {
278
+ "total_actions": sum(s.count for s in self._stats.values()),
279
+ "actions": {},
280
+ }
281
+
282
+ for action, stats in self._stats.items():
283
+ mean = stats.alpha / (stats.alpha + stats.beta) if (stats.alpha + stats.beta) > 0 else 0.5
284
+ summary["actions"][action] = {
285
+ "count": stats.count,
286
+ "success_rate": mean,
287
+ }
288
+
289
+ return summary
290
+
291
+ def _load(self):
292
+ if not self.memory_path or not os.path.exists(self.memory_path):
293
+ return
294
+
295
+ try:
296
+ with open(self.memory_path, "r", encoding="utf-8") as f:
297
+ data = json.load(f)
298
+
299
+ for action, stats_data in data.get("actions", {}).items():
300
+ if action in self._stats:
301
+ self._stats[action] = ActionStats.from_dict(stats_data)
302
+
303
+ logger.info("[ActionBandit] 加载统计数据: %s", self.memory_path)
304
+
305
+ except Exception as e:
306
+ logger.warning("[ActionBandit] 加载失败: %s", e)
307
+
308
+ def _save(self):
309
+ if not self.memory_path:
310
+ return
311
+
312
+ try:
313
+ Path(self.memory_path).parent.mkdir(parents=True, exist_ok=True)
314
+
315
+ data = {
316
+ "actions": self.get_stats(),
317
+ "session_count": self._session_count,
318
+ }
319
+
320
+ with open(self.memory_path, "w", encoding="utf-8") as f:
321
+ json.dump(data, f, indent=2, ensure_ascii=False)
322
+
323
+ except Exception as e:
324
+ logger.warning("[ActionBandit] 保存失败: %s", e)
325
+
326
+ def reset(self):
327
+ for action in self._stats:
328
+ self._stats[action] = ActionStats()
329
+ self._session_count = 0
330
+ self._reward_buffer.clear()
331
+ self._last_actions.clear()
332
+
333
+ if self.memory_path:
334
+ self._save()
335
+
336
+ logger.info("[ActionBandit] 统计已重置")
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Constraint Gene - 控制约束 Gene 子模块
4
+
5
+ 包含:
6
+ - TaskSignalMatcher: 任务信号匹配器
7
+ - GeneEvolution: Gene 进化系统
8
+ - GeneComposer: Gene 组合器
9
+ """
10
+
11
+ from .matcher import TaskSignalMatcher
12
+ from .evolution import GeneEvolution
13
+ from .composer import GeneComposer
14
+
15
+ __all__ = ["TaskSignalMatcher", "GeneEvolution", "GeneComposer"]
@@ -0,0 +1,158 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ GeneComposer - Gene 组合器
4
+
5
+ 职责:
6
+ 1. 多任务匹配
7
+ 2. Gene 组合与合并
8
+ """
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from .matcher import TaskSignalMatcher
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class GeneComposer:
19
+ """Gene 组合器 - 处理多任务匹配和组合"""
20
+
21
+ TASK_PRIORITY = {
22
+ "code_debug": 100,
23
+ "file_operation": 90,
24
+ "web_search": 80,
25
+ }
26
+
27
+ @classmethod
28
+ def match_multiple(cls, user_input: str) -> List[Dict[str, Any]]:
29
+ if not user_input:
30
+ return []
31
+
32
+ user_lower = user_input.lower()
33
+ matches = []
34
+
35
+ for task_type, config in TaskSignalMatcher._cache.items():
36
+ signals = config.get("signals", [])
37
+ if any(signal in user_lower for signal in signals):
38
+ matches.append({
39
+ "task_type": task_type,
40
+ "gene_template": config["gene_template"],
41
+ "forbidden_tools": config["forbidden_tools"],
42
+ "preferred_tools": config["preferred_tools"],
43
+ "priority": cls.TASK_PRIORITY.get(task_type, 50),
44
+ })
45
+
46
+ for task_type, config in TaskSignalMatcher.TASK_PATTERNS.items():
47
+ if any(signal in user_lower for signal in config["signals"]):
48
+ if not any(m["task_type"] == task_type for m in matches):
49
+ matches.append({
50
+ "task_type": task_type,
51
+ "gene_template": config["gene_template"],
52
+ "forbidden_tools": config["forbidden_tools"],
53
+ "preferred_tools": config["preferred_tools"],
54
+ "priority": cls.TASK_PRIORITY.get(task_type, 50),
55
+ })
56
+
57
+ matches.sort(key=lambda x: x["priority"], reverse=True)
58
+ return matches
59
+
60
+ @classmethod
61
+ def compose(cls, matches: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
62
+ if not matches:
63
+ return None
64
+
65
+ if len(matches) == 1:
66
+ return matches[0]
67
+
68
+ task_types = [m["task_type"] for m in matches]
69
+ combined_forbidden = list(set(
70
+ tool for m in matches for tool in m["forbidden_tools"]
71
+ ))
72
+ combined_preferred = list(set(
73
+ tool for m in matches for tool in m["preferred_tools"]
74
+ ))
75
+
76
+ hard_constraints = cls._build_combined_constraints(matches)
77
+
78
+ return {
79
+ "task_type": f"combined:{','.join(task_types)}",
80
+ "gene_template": hard_constraints,
81
+ "forbidden_tools": combined_forbidden,
82
+ "preferred_tools": combined_preferred,
83
+ "component_tasks": task_types,
84
+ }
85
+
86
+ @classmethod
87
+ def _build_combined_constraints(cls, matches: List[Dict[str, Any]]) -> str:
88
+ task_types = [m["task_type"] for m in matches]
89
+
90
+ lines = [
91
+ "[HARD CONSTRAINTS]",
92
+ f"Multi-task: {' + '.join(task_types)}",
93
+ "",
94
+ "[CONTROL ACTION]",
95
+ ]
96
+
97
+ for i, match in enumerate(matches, 1):
98
+ task_type = match["task_type"]
99
+ template = match["gene_template"]
100
+
101
+ must_section = cls._extract_section(template, "MUST:")
102
+ must_not_section = cls._extract_section(template, "MUST NOT:")
103
+
104
+ lines.append(f"STEP {i} [{task_type}]:")
105
+ if must_section:
106
+ lines.append(f" MUST: {must_section}")
107
+ if must_not_section:
108
+ lines.append(f" MUST NOT: {must_not_section}")
109
+ lines.append("")
110
+
111
+ avoid_items = set()
112
+ for match in matches:
113
+ template = match["gene_template"]
114
+ items = cls._extract_avoid_items(template)
115
+ avoid_items.update(items)
116
+
117
+ if avoid_items:
118
+ lines.append("[AVOID]")
119
+ for item in sorted(avoid_items):
120
+ lines.append(f"- {item}")
121
+
122
+ return "\n".join(lines)
123
+
124
+ @classmethod
125
+ def _extract_section(cls, template: str, marker: str) -> str:
126
+ lines = template.split("\n")
127
+ result = []
128
+ capturing = False
129
+
130
+ for line in lines:
131
+ if marker in line:
132
+ capturing = True
133
+ result.append(line.split(marker, 1)[-1].strip())
134
+ elif capturing:
135
+ if line.strip().startswith("MUST") or line.strip().startswith("["):
136
+ break
137
+ if line.strip():
138
+ result.append(line.strip())
139
+
140
+ return " ".join(result) if result else ""
141
+
142
+ @classmethod
143
+ def _extract_avoid_items(cls, template: str) -> List[str]:
144
+ lines = template.split("\n")
145
+ items = []
146
+ in_avoid = False
147
+
148
+ for line in lines:
149
+ if "[AVOID]" in line:
150
+ in_avoid = True
151
+ continue
152
+ if in_avoid:
153
+ if line.strip().startswith("["):
154
+ break
155
+ if line.strip().startswith("-"):
156
+ items.append(line.strip()[1:].strip())
157
+
158
+ return items