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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- 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)
|