cct-cli 0.7.9.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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,519 @@
1
+ import json
2
+ import re
3
+ import time
4
+ import os
5
+
6
+ from .tool_call_normalizer import ToolCall, normalize_tool_calls
7
+
8
+
9
+ def _fix_spacing(text):
10
+ """Post-processing repair for LLMs that return concatenated text
11
+ without spaces (e.g. 'Whatwouldyouliketodo?Youcanaskme...').
12
+
13
+ Only fires when the text looks concatenated — normal text with
14
+ proper spacing passes through untouched. Never modifies fenced
15
+ code blocks, inline code, URLs, or LaTeX."""
16
+ if not text or not isinstance(text, str):
17
+ return text
18
+ if "```" in text:
19
+ parts = text.split("```")
20
+ for i in range(0, len(parts), 2):
21
+ parts[i] = _fix_spacing_segment(parts[i])
22
+ return "```".join(parts)
23
+ return _fix_spacing_segment(text)
24
+
25
+
26
+ def _fix_spacing_segment(text):
27
+ if not text:
28
+ return text
29
+ lines = text.split("\n")
30
+ out = []
31
+ for line in lines:
32
+ if not line.strip():
33
+ out.append(line)
34
+ continue
35
+ stripped = line.strip()
36
+ if len(stripped) < 8:
37
+ out.append(line)
38
+ continue
39
+ spaces = sum(1 for c in stripped if c == " ")
40
+ if spaces == 0 and len(stripped) > 12:
41
+ fixed = _fix_concatenated_line(stripped)
42
+ out.append(line.replace(stripped, fixed))
43
+ else:
44
+ out.append(line)
45
+ return "\n".join(out)
46
+
47
+
48
+ _CONTRACTIONS = {
49
+ "I'm": "I am", "I've": "I have", "I'll": "I will", "I'd": "I would",
50
+ "you're": "you are", "you've": "you have", "you'll": "you will",
51
+ "you'd": "you would", "he's": "he is", "he'll": "he will",
52
+ "he'd": "he would", "she's": "she is", "she'll": "she will",
53
+ "she'd": "she would", "it's": "it is", "it'll": "it will",
54
+ "we're": "we are", "we've": "we have", "we'll": "we will",
55
+ "we'd": "we would", "they're": "they are", "they've": "they have",
56
+ "they'll": "they will", "they'd": "they would", "that's": "that is",
57
+ "who's": "who is", "what's": "what is", "where's": "where is",
58
+ "when's": "when is", "how's": "how is", "let's": "let us",
59
+ "can't": "cannot", "won't": "will not", "don't": "do not",
60
+ "doesn't": "does not", "didn't": "did not", "isn't": "is not",
61
+ "aren't": "are not", "wasn't": "was not", "weren't": "were not",
62
+ "hasn't": "has not", "haven't": "have not", "hadn't": "had not",
63
+ "couldn't": "could not", "shouldn't": "should not",
64
+ "wouldn't": "would not", "mustn't": "must not",
65
+ }
66
+
67
+
68
+ def _fix_concatenated_line(s):
69
+ for contraction, expansion in _CONTRACTIONS.items():
70
+ s = s.replace(contraction, expansion)
71
+ t = re.sub(r"([a-z])([A-Z][a-z])", r"\1 \2", s)
72
+ t = re.sub(r"([a-z])([A-Z][A-Z])", r"\1 \2", t)
73
+ t = re.sub(r"([a-z,;)])([A-Z])", r"\1 \2", t)
74
+ t = re.sub(r"([a-z])(\()", r"\1 \2", t)
75
+ t = re.sub(r"(\))([a-zA-Z])", r"\1 \2", t)
76
+ t = re.sub(r"([.!?:;,])([A-Za-z])", r"\1 \2", t)
77
+ parts = re.split(r"([.!?:;,])", t)
78
+ result = []
79
+ for part in parts:
80
+ if not part:
81
+ continue
82
+ if re.match(r"^[.!?:;,]$", part):
83
+ result.append(part)
84
+ continue
85
+ subparts = part.split(" ")
86
+ fixed_subparts = []
87
+ for sp in subparts:
88
+ if not sp:
89
+ fixed_subparts.append(sp)
90
+ continue
91
+ if " " in sp or len(sp) <= 5:
92
+ fixed_subparts.append(sp)
93
+ continue
94
+ words = _segment_words(sp)
95
+ fixed_subparts.append(" ".join(words) if len(words) > 1 else sp)
96
+ result.append(" ".join(fixed_subparts))
97
+ out = "".join(result)
98
+ out = re.sub(r" +", " ", out)
99
+ return out.strip()
100
+
101
+
102
+ _WORDS = frozenset([
103
+ "a", "i", "an", "is", "it", "in", "on", "to", "do", "no", "so", "if",
104
+ "or", "be", "by", "he", "me", "my", "up", "we", "us", "am", "as", "at",
105
+ "of", "the", "and", "for", "are", "but", "not", "you", "all", "can",
106
+ "had", "her", "was", "one", "our", "out", "has", "his", "how", "its",
107
+ "may", "new", "now", "old", "see", "way", "who", "did", "get", "let",
108
+ "say", "she", "too", "use", "what", "when", "your", "will", "with",
109
+ "have", "from", "they", "been", "each", "make", "like", "long", "look",
110
+ "many", "some", "than", "them", "then", "these", "time", "very",
111
+ "just", "over", "such", "take", "year", "into", "also", "back",
112
+ "that", "this", "which", "would", "could", "about", "there", "their",
113
+ "other", "after", "first", "being", "where", "still", "every", "world",
114
+ "those", "ask", "help", "problem", "discuss", "coding", "request",
115
+ "question", "scientific", "topic", "know", "think", "need", "want",
116
+ "try", "give", "come", "here", "real", "own", "same", "tell", "work",
117
+ "plan", "show", "find", "keep", "let", "run", "set", "go", "open",
118
+ "read", "write", "edit", "file", "code", "test", "build", "fix",
119
+ "error", "debug", "print", "loop", "list", "type", "class", "func",
120
+ "true", "false", "null", "none", "self", "data", "text", "name",
121
+ "path", "line", "char", "byte", "int", "str", "bool", "float",
122
+ "def", "return", "import", "from", "class", "while", "break",
123
+ "pass", "else", "elif", "except", "raise", "try", "finally",
124
+ "welcome", "am", "here", "assist", "today", "ready", "start",
125
+ "hello", "hey", "hi", "yes", "yeah", "sure", "okay", "ok",
126
+ "please", "thank", "thanks", "sorry", "good", "bad", "well",
127
+ "also", "just", "only", "even", "still", "already", "yet",
128
+ "really", "quite", "right", "well", "too", "much", "more",
129
+ "most", "less", "least", "very", "quite", "rather", "pretty",
130
+ "sure", "happy", "glad", "able", "unable", "possible", "impossible",
131
+ "need", "want", "like", "love", "hate", "prefer", "choose",
132
+ "make", "create", "build", "write", "read", "run", "use",
133
+ "can", "could", "will", "would", "shall", "should", "may",
134
+ "might", "must", "ought", "dare", "need", "used",
135
+ "tasks", "task", "work", "done", "doing", "do", "working",
136
+ "about", "where", "there", "their", "which", "what", "when",
137
+ "while", "about", "after", "before", "during", "through",
138
+ "being", "having", "going", "coming", "making", "doing",
139
+ "seeing", "saying", "telling", "asking", "helping", "using",
140
+ "coding", "scientist", "assistant", "terminal", "agent",
141
+ ])
142
+
143
+
144
+ def _segment_words(s):
145
+ n = len(s)
146
+ dp = [None] * (n + 1)
147
+ dp[0] = []
148
+ for i in range(n):
149
+ if dp[i] is None:
150
+ continue
151
+ for end in range(i + 1, min(i + 20, n + 1)):
152
+ word = s[i:end].lower()
153
+ if word in _WORDS:
154
+ if dp[end] is None or len(dp[end]) > len(dp[i]) + 1:
155
+ dp[end] = dp[i] + [s[i:end]]
156
+ if dp[n]:
157
+ return dp[n]
158
+ return [s]
159
+
160
+
161
+ class AgentRuntime:
162
+ def __init__(self, tools_dict, max_steps=18):
163
+ self.tools = tools_dict
164
+ self.max_steps = max_steps
165
+
166
+ def _build_prompt(self, convo):
167
+ lines = []
168
+ for turn in convo:
169
+ lines.append(f"{turn['role'].upper()}: {turn['text']}")
170
+ lines.append("ASSISTANT:")
171
+ return "\n\n".join(lines)
172
+
173
+ def _normalize_action(self, action, raw_response=""):
174
+ if not isinstance(action, dict):
175
+ return None
176
+
177
+ tool_keys = [k for k in action.keys() if k in self.tools]
178
+ if len(tool_keys) == 1 and "action" not in action and "tool" not in action:
179
+ tool_name = tool_keys[0]
180
+ tool_args = action[tool_name]
181
+ if isinstance(tool_args, dict):
182
+ return {
183
+ "action": "tool",
184
+ "tool": tool_name,
185
+ "args": tool_args,
186
+ "thought": action.get("thought") or "Corrected from implicit tool call format.",
187
+ }
188
+
189
+ act = action.get("action")
190
+ reserved = {"action", "tool", "args", "thought"}
191
+
192
+ if act == "final" or act == "tool":
193
+ return action
194
+
195
+ if act is None and isinstance(action.get("tool"), str) and action["tool"] in self.tools:
196
+ args = action.get("args")
197
+ if not isinstance(args, dict):
198
+ args = {k: v for k, v in action.items() if k not in reserved}
199
+ return {"action": "tool", "tool": action["tool"], "args": args,
200
+ "thought": action.get("thought")}
201
+
202
+ if isinstance(act, str) and act in self.tools:
203
+ args = action.get("args")
204
+ if not isinstance(args, dict):
205
+ args = {k: v for k, v in action.items() if k not in reserved}
206
+ return {"action": "tool", "tool": act, "args": args,
207
+ "thought": action.get("thought")}
208
+
209
+ for key in ("text", "answer", "response", "message"):
210
+ if isinstance(action.get(key), str):
211
+ return {"action": "final", "text": action[key]}
212
+
213
+ return action
214
+
215
+ def _find_balanced_json(self, text, start=0):
216
+ if start >= len(text) or text[start] != "{":
217
+ return None
218
+ depth = 0
219
+ in_string = False
220
+ escape = False
221
+ for i in range(start, len(text)):
222
+ c = text[i]
223
+ if escape:
224
+ escape = False
225
+ continue
226
+ if c == "\\":
227
+ escape = True
228
+ continue
229
+ if c == '"':
230
+ in_string = not in_string
231
+ continue
232
+ if in_string:
233
+ continue
234
+ if c == "{":
235
+ depth += 1
236
+ elif c == "}":
237
+ depth -= 1
238
+ if depth == 0:
239
+ return text[start : i + 1]
240
+ return None
241
+
242
+ def _extract_json(self, text):
243
+ if not text:
244
+ return None
245
+ text = text.strip()
246
+ text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
247
+ m = re.search(r"\{.*\}", text, re.S)
248
+ if not m:
249
+ return None
250
+ candidate = m.group(0)
251
+ try:
252
+ return json.loads(candidate)
253
+ except Exception:
254
+ depth = 0
255
+ for i, ch in enumerate(candidate):
256
+ if ch == "{":
257
+ depth += 1
258
+ elif ch == "}":
259
+ depth -= 1
260
+ if depth == 0:
261
+ try:
262
+ return json.loads(candidate[: i + 1])
263
+ except Exception:
264
+ return None
265
+ return None
266
+
267
+ def _extract_protocol_text(self, data):
268
+ if not isinstance(data, dict):
269
+ return None
270
+ if isinstance(data.get("action"), str):
271
+ payload = data.get("text") or data.get("answer") \
272
+ or data.get("response") or data.get("message")
273
+ return payload if isinstance(payload, str) else None
274
+ for key in ("text", "answer", "response", "message"):
275
+ if isinstance(data.get(key), str):
276
+ return data[key]
277
+ return None
278
+
279
+ def _balanced_json_spans(self, text):
280
+ spans = []
281
+ depth = 0
282
+ start = -1
283
+ for i, ch in enumerate(text):
284
+ if ch == "{":
285
+ if depth == 0:
286
+ start = i
287
+ depth += 1
288
+ elif ch == "}":
289
+ if depth > 0:
290
+ depth -= 1
291
+ if depth == 0 and start >= 0:
292
+ spans.append((start, i + 1))
293
+ start = -1
294
+ return spans
295
+
296
+ def _is_protocol_blob(self, data):
297
+ return isinstance(data, dict) and isinstance(data.get("action"), str)
298
+
299
+ def clean_final_text(self, text):
300
+ if not text or not isinstance(text, str):
301
+ return text or ""
302
+ stripped = text.strip()
303
+ if not stripped:
304
+ return ""
305
+
306
+ m = re.search(r"\{.*\}", stripped, re.S)
307
+ if m and len(m.group(0).replace(" ", "")) >= len(stripped.replace(" ", "")) * 0.7:
308
+ try:
309
+ data = json.loads(m.group(0))
310
+ payload = self._extract_protocol_text(data)
311
+ if payload and payload.strip():
312
+ return payload.strip()
313
+ except Exception:
314
+ pass
315
+
316
+ out = []
317
+ cursor = 0
318
+ changed = False
319
+ for start, end in self._balanced_json_spans(text):
320
+ chunk = text[start:end]
321
+ try:
322
+ data = json.loads(chunk)
323
+ except Exception:
324
+ continue
325
+ if not self._is_protocol_blob(data):
326
+ continue
327
+ out.append(text[cursor:start])
328
+ cursor = end
329
+ changed = True
330
+ if changed:
331
+ out.append(text[cursor:])
332
+ cleaned = "".join(out)
333
+ cleaned = re.sub(r"[ \t]+\n", "\n", cleaned)
334
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
335
+ return _fix_spacing(cleaned or text.strip())
336
+ return _fix_spacing(text)
337
+
338
+ def _check_permission(self, name, args, permission_callback):
339
+ spec = self.tools[name]
340
+ perm_key = spec.get("perm_key")
341
+ if not perm_key:
342
+ return True, None
343
+
344
+ from . import permissions as perm
345
+ if perm.manager.refused_by_always_deny(perm_key):
346
+ return False, "Blocked by permission setting — previously denied for this session."
347
+ needs = perm.manager.needs_prompt(perm_key)
348
+ if not needs:
349
+ return True, None
350
+
351
+ describe = spec.get("describe")
352
+ if describe:
353
+ action_label, path, reason = describe(args)
354
+ else:
355
+ action_label, path, reason = name, str(args.get("path", "")), "Requested by the AI."
356
+
357
+ if permission_callback:
358
+ decision = permission_callback(perm_key, action_label, path, reason)
359
+ else:
360
+ decision = "allow_once"
361
+
362
+ proceed = perm.manager.decide(perm_key, decision, action_label, reason)
363
+ if proceed:
364
+ return True, None
365
+
366
+ verb = "Rejected" if perm.manager.restricted_review(perm_key) else "Denied"
367
+ return False, (f"{verb} by the user \u2014 did not {action_label.lower()}. "
368
+ f"Do not retry this exact action; tell the user it was declined "
369
+ f"and continue only if there's another way to help.")
370
+
371
+ def run(self, user_text, query_fn, system_prompt, on_step=None,
372
+ permission_callback=None, should_cancel=None, history=None):
373
+ def _cancelled():
374
+ try:
375
+ return bool(should_cancel and should_cancel())
376
+ except Exception:
377
+ return False
378
+
379
+ convo = list(history) if history else []
380
+ convo.append({"role": "user", "text": user_text})
381
+ steps = []
382
+ last_call = None
383
+ stall_count = 0
384
+
385
+ def _finish(text):
386
+ meta = {
387
+ "mode": "agent",
388
+ "suggest_agent": False,
389
+ "file_changes": "",
390
+ }
391
+ return self.clean_final_text(text), steps, meta
392
+
393
+ for i in range(self.max_steps):
394
+ if _cancelled():
395
+ return _finish(
396
+ "*(interrupted \u2014 here is everything completed so far)*\n"
397
+ + ("\n".join(f"- {n}: {o}" for n, _, o, _c in steps)
398
+ if steps else "No tools had run yet."))
399
+
400
+ prompt = self._build_prompt(convo)
401
+ raw = query_fn(prompt, system_prompt=system_prompt)
402
+ if not raw or not raw.strip():
403
+ raw = query_fn(prompt, system_prompt=system_prompt)
404
+
405
+ tool_calls = normalize_tool_calls(raw, list(self.tools.keys()))
406
+ if tool_calls:
407
+ tc = tool_calls[0]
408
+ name = tc.name
409
+ args = tc.arguments if isinstance(tc.arguments, dict) else {}
410
+ spec = self.tools.get(name)
411
+
412
+ fingerprint = (name, json.dumps(args, sort_keys=True, default=str))
413
+ stall_count = stall_count + 1 if fingerprint == last_call else 0
414
+ last_call = fingerprint
415
+
416
+ if not spec:
417
+ obs = f"Tool '{name}' does not exist. Valid tools: {', '.join(self.tools)}"
418
+ elif stall_count >= 1:
419
+ obs = ("You already ran this exact tool call and got a result \u2014 repeating it "
420
+ "won't help. Use that result and move on to the NEXT step, or if every "
421
+ "part of the question is now answered, reply with the final JSON action.")
422
+ else:
423
+ if on_step:
424
+ on_step(name, args)
425
+ if _cancelled():
426
+ return _finish(
427
+ "*(interrupted \u2014 here is everything completed so far)*\n"
428
+ + ("\n".join(f"- {n}: {o}" for n, _, o, _c in steps)
429
+ if steps else "No tools had run yet."))
430
+ proceed, denial_obs = self._check_permission(name, args, permission_callback)
431
+ if not proceed:
432
+ obs = denial_obs
433
+ else:
434
+ try:
435
+ obs = spec["run"](args)
436
+ except Exception as e:
437
+ obs = f"Tool '{name}' raised an error: {e}"
438
+ change = {}
439
+ steps.append((name, args, obs, change))
440
+
441
+ convo.append({"role": "assistant", "text": raw})
442
+ steps_left = self.max_steps - i - 1
443
+ nudge = ""
444
+ if steps_left <= 3:
445
+ nudge = (f"\n\n(You have about {steps_left} step(s) left \u2014 start wrapping up: "
446
+ "finish any remaining sub-calculations now and prepare the final answer.")
447
+ convo.append({"role": "user", "text": (
448
+ f"TOOL RESULT [{name}]: {obs}{nudge}\n\n"
449
+ "Continue: call another tool if needed, or reply with the final JSON action now."
450
+ )})
451
+ continue
452
+
453
+ action = self._normalize_action(self._extract_json(raw), raw)
454
+ if action and action.get("action") == "final":
455
+ text = action.get("text") or raw
456
+ return _finish(text)
457
+ if action and action.get("action") == "tool":
458
+ name = action.get("tool")
459
+ args = action.get("args") or {}
460
+ if not isinstance(args, dict):
461
+ args = {}
462
+ spec = self.tools.get(name)
463
+
464
+ fingerprint = (name, json.dumps(args, sort_keys=True, default=str))
465
+ stall_count = stall_count + 1 if fingerprint == last_call else 0
466
+ last_call = fingerprint
467
+
468
+ if not spec:
469
+ obs = f"Tool '{name}' does not exist. Valid tools: {', '.join(self.tools)}"
470
+ elif stall_count >= 1:
471
+ obs = ("You already ran this exact tool call and got a result \u2014 repeating it "
472
+ "won't help. Use that result and move on to the NEXT step, or if every "
473
+ "part of the question is now answered, reply with the final JSON action.")
474
+ else:
475
+ if on_step:
476
+ on_step(name, args)
477
+ if _cancelled():
478
+ return _finish(
479
+ "*(interrupted \u2014 here is everything completed so far)*\n"
480
+ + ("\n".join(f"- {n}: {o}" for n, _, o, _c in steps)
481
+ if steps else "No tools had run yet."))
482
+ proceed, denial_obs = self._check_permission(name, args, permission_callback)
483
+ if not proceed:
484
+ obs = denial_obs
485
+ else:
486
+ try:
487
+ obs = spec["run"](args)
488
+ except Exception as e:
489
+ obs = f"Tool '{name}' raised an error: {e}"
490
+ change = {}
491
+ steps.append((name, args, obs, change))
492
+
493
+ convo.append({"role": "assistant", "text": raw})
494
+ steps_left = self.max_steps - i - 1
495
+ nudge = ""
496
+ if steps_left <= 3:
497
+ nudge = (f"\n\n(You have about {steps_left} step(s) left \u2014 start wrapping up: "
498
+ "finish any remaining sub-calculations now and prepare the final answer.")
499
+ convo.append({"role": "user", "text": (
500
+ f"TOOL RESULT [{name}]: {obs}{nudge}\n\n"
501
+ "Continue: call another tool if needed, or reply with the final JSON action now."
502
+ )})
503
+ continue
504
+
505
+ return _finish(raw or "I couldn't reach the AI provider. Try again.")
506
+
507
+ convo.append({"role": "user", "text": (
508
+ "You're out of tool-call steps. Using every result computed above, give your single "
509
+ "best complete answer to the ORIGINAL question now \u2014 reply with the final "
510
+ 'JSON action: {"action": "final", "text": "<complete answer, every part labeled>"}.'
511
+ )})
512
+ raw = query_fn(self._build_prompt(convo), system_prompt=system_prompt)
513
+ action = self._normalize_action(self._extract_json(raw), raw)
514
+ if action and action.get("action") == "final" and action.get("text"):
515
+ return _finish(action["text"])
516
+ if raw and raw.strip():
517
+ return _finish(raw)
518
+ summary = "\n".join(f"- {n}: {o}" for n, _, o, _c in steps) or "No tools were run."
519
+ return _finish("Here's everything computed so far while working on that:\n" + summary)