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,233 @@
|
|
|
1
|
+
"""Background Ollama download manager with progress streaming.
|
|
2
|
+
Uses Ollama's POST /api/pull streaming JSON lines.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import urllib.request
|
|
8
|
+
import urllib.error
|
|
9
|
+
from typing import Optional, Dict, Any, Tuple
|
|
10
|
+
|
|
11
|
+
def ollama_base_url():
|
|
12
|
+
try:
|
|
13
|
+
from . import config as _cfg
|
|
14
|
+
return _cfg.load_config().ollama_endpoint.rstrip("/")
|
|
15
|
+
except Exception:
|
|
16
|
+
return "http://localhost:11434"
|
|
17
|
+
|
|
18
|
+
def is_ollama_installed():
|
|
19
|
+
import shutil
|
|
20
|
+
return bool(shutil.which("ollama"))
|
|
21
|
+
|
|
22
|
+
def is_ollama_running(base_url=None, timeout=3):
|
|
23
|
+
import urllib.request
|
|
24
|
+
base = (base_url or ollama_base_url()).rstrip("/")
|
|
25
|
+
try:
|
|
26
|
+
with urllib.request.urlopen(f"{base}/api/tags", timeout=timeout) as r:
|
|
27
|
+
return r.status in (200, 204)
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def ensure_ollama_running(base_url=None, timeout=12, auto_start=True):
|
|
32
|
+
"""Ensure Ollama is reachable; if not and auto_start, try `ollama serve`."""
|
|
33
|
+
if is_ollama_running(base_url, timeout=3):
|
|
34
|
+
return True, "already running"
|
|
35
|
+
if not auto_start:
|
|
36
|
+
return False, "not running"
|
|
37
|
+
if not is_ollama_installed():
|
|
38
|
+
return False, "Ollama not installed — get it from https://ollama.com/download"
|
|
39
|
+
# Try to start ollama serve as background process
|
|
40
|
+
try:
|
|
41
|
+
import subprocess, shutil, sys, os
|
|
42
|
+
ollama_bin = shutil.which("ollama")
|
|
43
|
+
if not ollama_bin:
|
|
44
|
+
return False, "ollama binary not found"
|
|
45
|
+
# Use CREATE_NO_WINDOW on Windows to avoid popup
|
|
46
|
+
kwargs = {}
|
|
47
|
+
if sys.platform == "win32":
|
|
48
|
+
try:
|
|
49
|
+
import subprocess as sp
|
|
50
|
+
kwargs["creationflags"] = getattr(sp, "CREATE_NO_WINDOW", 0)
|
|
51
|
+
si = sp.STARTUPINFO()
|
|
52
|
+
si.dwFlags |= sp.STARTF_USESHOWWINDOW
|
|
53
|
+
kwargs["startupinfo"] = si
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
else:
|
|
57
|
+
kwargs["start_new_session"] = True
|
|
58
|
+
# Start serve detached
|
|
59
|
+
try:
|
|
60
|
+
subprocess.Popen([ollama_bin, "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return False, f"failed to start: {e}"
|
|
63
|
+
# Wait for it to be reachable
|
|
64
|
+
import time
|
|
65
|
+
for _ in range(timeout):
|
|
66
|
+
time.sleep(1)
|
|
67
|
+
if is_ollama_running(base_url, timeout=2):
|
|
68
|
+
return True, "started"
|
|
69
|
+
return False, "started but not reachable yet — wait a moment and retry"
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return False, str(e)
|
|
72
|
+
|
|
73
|
+
def installed_models(base_url=None):
|
|
74
|
+
"""Return set of installed model names via /api/tags."""
|
|
75
|
+
base = (base_url or ollama_base_url()).rstrip("/")
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(f"{base}/api/tags", timeout=10) as r:
|
|
78
|
+
data = json.loads(r.read().decode("utf-8"))
|
|
79
|
+
return {m.get("name","") for m in data.get("models",[]) if m.get("name")}
|
|
80
|
+
except Exception:
|
|
81
|
+
return set()
|
|
82
|
+
|
|
83
|
+
def pull_model(model_name, on_progress=None, on_done=None, base_url=None):
|
|
84
|
+
"""
|
|
85
|
+
Background pull. Calls on_progress dict with keys:
|
|
86
|
+
status, completed, total, percent, speed, eta_seconds
|
|
87
|
+
Streaming is done on a daemon thread.
|
|
88
|
+
Returns thread.
|
|
89
|
+
"""
|
|
90
|
+
base = (base_url or ollama_base_url()).rstrip("/")
|
|
91
|
+
url = f"{base}/api/pull"
|
|
92
|
+
|
|
93
|
+
def _worker():
|
|
94
|
+
payload = json.dumps({"name": model_name, "stream": True}).encode("utf-8")
|
|
95
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
96
|
+
req.add_header("Content-Type","application/json")
|
|
97
|
+
start = time.time()
|
|
98
|
+
last_completed = 0
|
|
99
|
+
last_time = start
|
|
100
|
+
try:
|
|
101
|
+
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
102
|
+
# Ollama streams JSON lines: {"status":"pulling manifest", "completed":..., "total":...}
|
|
103
|
+
buf = b""
|
|
104
|
+
for chunk in iter(lambda: resp.read(8192), b""):
|
|
105
|
+
if not chunk:
|
|
106
|
+
break
|
|
107
|
+
buf += chunk
|
|
108
|
+
while b"\n" in buf:
|
|
109
|
+
line, buf = buf.split(b"\n",1)
|
|
110
|
+
line=line.strip()
|
|
111
|
+
if not line:
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
js = json.loads(line.decode("utf-8"))
|
|
115
|
+
except Exception:
|
|
116
|
+
continue
|
|
117
|
+
status = js.get("status","")
|
|
118
|
+
completed = js.get("completed")
|
|
119
|
+
total = js.get("total")
|
|
120
|
+
percent = None
|
|
121
|
+
if isinstance(completed,int) and isinstance(total,int) and total>0:
|
|
122
|
+
percent = int(completed*100/total)
|
|
123
|
+
# speed / eta
|
|
124
|
+
now = time.time()
|
|
125
|
+
dt = now - last_time
|
|
126
|
+
speed = None
|
|
127
|
+
eta = None
|
|
128
|
+
if isinstance(completed,int) and dt>0.5:
|
|
129
|
+
delta = completed - last_completed
|
|
130
|
+
if delta>0:
|
|
131
|
+
speed = delta/dt # bytes/s
|
|
132
|
+
if isinstance(total,int) and total>completed and speed>0:
|
|
133
|
+
eta = int((total - completed)/speed)
|
|
134
|
+
last_completed = completed if isinstance(completed,int) else last_completed
|
|
135
|
+
last_time = now
|
|
136
|
+
prog = {"status": status, "completed": completed, "total": total, "percent": percent, "speed": speed, "eta": eta, "raw": js}
|
|
137
|
+
if on_progress:
|
|
138
|
+
try:
|
|
139
|
+
on_progress(prog)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
if status and "success" in status.lower():
|
|
143
|
+
break
|
|
144
|
+
if on_done:
|
|
145
|
+
try:
|
|
146
|
+
on_done(True, None)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
except urllib.error.HTTPError as e:
|
|
150
|
+
msg = f"HTTP {e.code}"
|
|
151
|
+
try:
|
|
152
|
+
body = e.read().decode("utf-8")
|
|
153
|
+
j = json.loads(body)
|
|
154
|
+
msg = j.get("error", msg)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
if on_done:
|
|
158
|
+
try:
|
|
159
|
+
on_done(False, msg)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
except Exception as e:
|
|
163
|
+
if on_done:
|
|
164
|
+
try:
|
|
165
|
+
on_done(False, str(e))
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
th = threading.Thread(target=_worker, daemon=True, name=f"ollama-pull-{model_name}")
|
|
170
|
+
th.start()
|
|
171
|
+
return th
|
|
172
|
+
|
|
173
|
+
def estimate_minutes(size_gb, speed_mbps=50):
|
|
174
|
+
"""Very rough estimate: size_gb at speed_mbps megabits."""
|
|
175
|
+
try:
|
|
176
|
+
# speed in MB/s
|
|
177
|
+
mbps = speed_mbps # megabits
|
|
178
|
+
mbytes = float(size_gb)*1024
|
|
179
|
+
# assume 50 Mbps ~ 6.25 MB/s
|
|
180
|
+
secs = mbytes / (mbps/8) if mbps>0 else 0
|
|
181
|
+
return max(1, int(secs/60))
|
|
182
|
+
except Exception:
|
|
183
|
+
return 5
|
|
184
|
+
|
|
185
|
+
def pull_model_sync(model_name, base_url=None):
|
|
186
|
+
"""Blocking pull for CLI, returns (ok, msg)."""
|
|
187
|
+
done = {}
|
|
188
|
+
def _done(ok, msg):
|
|
189
|
+
done["ok"]=ok
|
|
190
|
+
done["msg"]=msg
|
|
191
|
+
th = pull_model(model_name, base_url=base_url, on_done=lambda ok,msg: done.update(ok=ok,msg=msg))
|
|
192
|
+
th.join(timeout=600)
|
|
193
|
+
return done.get("ok", False), done.get("msg")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def delete_model(model_name: str, base_url=None) -> tuple[bool, Optional[str]]:
|
|
197
|
+
"""Delete a model locally via Ollama DELETE /api/delete."""
|
|
198
|
+
base = (base_url or ollama_base_url()).rstrip("/")
|
|
199
|
+
url = f"{base}/api/delete"
|
|
200
|
+
payload = json.dumps({"name": model_name}).encode("utf-8")
|
|
201
|
+
req = urllib.request.Request(url, data=payload, method="DELETE")
|
|
202
|
+
req.add_header("Content-Type", "application/json")
|
|
203
|
+
try:
|
|
204
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
205
|
+
if resp.status in (200, 204):
|
|
206
|
+
return True, None
|
|
207
|
+
return False, f"HTTP {resp.status}"
|
|
208
|
+
except urllib.error.HTTPError as e:
|
|
209
|
+
msg = f"HTTP {e.code}"
|
|
210
|
+
try:
|
|
211
|
+
body = e.read().decode("utf-8")
|
|
212
|
+
j = json.loads(body)
|
|
213
|
+
msg = j.get("error", msg)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return False, msg
|
|
217
|
+
except Exception as e:
|
|
218
|
+
return False, str(e)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def inspect_model(model_name: str, base_url=None) -> Optional[dict]:
|
|
222
|
+
"""Retrieve detailed model metadata via Ollama POST /api/show."""
|
|
223
|
+
base = (base_url or ollama_base_url()).rstrip("/")
|
|
224
|
+
url = f"{base}/api/show"
|
|
225
|
+
payload = json.dumps({"name": model_name}).encode("utf-8")
|
|
226
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
227
|
+
req.add_header("Content-Type", "application/json")
|
|
228
|
+
try:
|
|
229
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
230
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
231
|
+
except Exception:
|
|
232
|
+
return None
|
|
233
|
+
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT — orchestrator.py: Agent Orchestrator (v0.7.7 spec section 9).
|
|
3
|
+
|
|
4
|
+
A Coordinator assigns work to specialist agents, and the Coordinator
|
|
5
|
+
continuously monitors them. This layers on top of agent.py's existing
|
|
6
|
+
multi-agent team (researcher/planner/specialist/programmer/debugger/
|
|
7
|
+
tester — see the "Multi-agent collaboration" section of agent.py) the
|
|
8
|
+
spec's remaining specialist roles:
|
|
9
|
+
|
|
10
|
+
Coordinator — decides the roster, watches every stage, reports
|
|
11
|
+
Planner — breaks the request into ordered sub-tasks
|
|
12
|
+
Research — gathers outside context (live web search)
|
|
13
|
+
Coding — executes real tools / writes code
|
|
14
|
+
Reviewer — cross-checks the coding agent's work for gaps
|
|
15
|
+
Testing — spot-checks numbers + sandbox-runs generated code
|
|
16
|
+
Security — real static security scan of generated code
|
|
17
|
+
Documentation — writes the summary/report pass
|
|
18
|
+
Performance — flags obvious performance problems in generated code
|
|
19
|
+
UI — reviews UI-facing suggestions (only for UI requests)
|
|
20
|
+
Architecture — reviews structure/design of the proposed solution
|
|
21
|
+
|
|
22
|
+
Same honest architecture note as agent.py: CCT talks to one configured
|
|
23
|
+
provider, so "agents" are specialized prompt passes over that provider,
|
|
24
|
+
sequenced by the Coordinator, with the Coding agent running the real
|
|
25
|
+
tool loop. The Coordinator is the only object that decides which
|
|
26
|
+
specialists join and in what order; it reports every agent start/finish
|
|
27
|
+
through `on_agent(role_key, role_label, status)` so the UI can show a
|
|
28
|
+
live roster.
|
|
29
|
+
|
|
30
|
+
The orchestrated run returns a full record:
|
|
31
|
+
final_text, steps, meta, agents where `agents` is the ordered list
|
|
32
|
+
of {key, label, status, summary} the coordinator monitored.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
print("This is a library file and is not meant to be run directly.")
|
|
37
|
+
import sys
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
from . import aicore
|
|
44
|
+
from . import agent
|
|
45
|
+
from . import security_scanner
|
|
46
|
+
from . import sound
|
|
47
|
+
from . import event_stream
|
|
48
|
+
|
|
49
|
+
# All spec-listed specialist roles. `matches` is a content gate — the
|
|
50
|
+
# role only joins the roster when the request actually needs it.
|
|
51
|
+
SPECIALIST_ROLES = {
|
|
52
|
+
"coordinator": ("Coordinator", "assigns work and monitors every other agent"),
|
|
53
|
+
"planner": ("Planner", "breaks the request into an ordered task list"),
|
|
54
|
+
"research": ("Research", "gathers outside context via live web search"),
|
|
55
|
+
"coding": ("Coding", "executes real tools to do the work"),
|
|
56
|
+
"reviewer": ("Reviewer", "cross-checks the result against the request"),
|
|
57
|
+
"testing": ("Testing", "spot-checks numbers and sandbox-runs generated code"),
|
|
58
|
+
"security": ("Security", "static security scan of any generated code"),
|
|
59
|
+
"documentation": ("Documentation", "writes the final report"),
|
|
60
|
+
"performance": ("Performance", "flags performance problems in generated code"),
|
|
61
|
+
"ui": ("UI", "reviews UI-facing suggestions"),
|
|
62
|
+
"architecture": ("Architecture", "reviews the design structure"),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_CODE_HINTS = re.compile(
|
|
66
|
+
r"\b(code|python|script|program|function|write a .*(class|module)|"
|
|
67
|
+
r"generate.*(code|script))\b", re.I)
|
|
68
|
+
_UI_HINTS = re.compile(
|
|
69
|
+
r"\b(ui|interface|screen|layout|widget|button|menu|dashboard|"
|
|
70
|
+
r"frontend|web page|landing page|design)\b", re.I)
|
|
71
|
+
_ARCH_HINTS = re.compile(
|
|
72
|
+
r"\b(architecture|design|structure|module|layers?|service|"
|
|
73
|
+
r"microservice|api design|database schema|data model)\b", re.I)
|
|
74
|
+
_PERF_HINTS = re.compile(
|
|
75
|
+
r"\b(performance|fast|speed up|optimize|slow|efficient|benchmark)\b", re.I)
|
|
76
|
+
|
|
77
|
+
_REVIEWER_PROMPT = """You are the REVIEWER on an AI team working inside CCT
|
|
78
|
+
(Chemistry Calc Terminal). You are handed the ORIGINAL request and the
|
|
79
|
+
CODING agent's final answer. Look for: parts of the request left
|
|
80
|
+
unanswered, implied assumptions that were never stated, and results
|
|
81
|
+
that don't obviously follow from the given data. Reply with ONLY a JSON
|
|
82
|
+
object:
|
|
83
|
+
{"ok": true, "note": ""} if the answer covers the request, or
|
|
84
|
+
{"ok": false, "note": "<what's missing or wrong, one sentence>"}."""
|
|
85
|
+
|
|
86
|
+
_PERFORMANCE_PROMPT = """You are the PERFORMANCE agent on an AI team working
|
|
87
|
+
inside CCT. You are handed the generated code (or solution). Identify at
|
|
88
|
+
most 3 concrete performance problems (algorithmic complexity, repeated
|
|
89
|
+
work, I/O patterns) — no fluff. Reply with ONLY a JSON object:
|
|
90
|
+
{"ok": true, "note": "no significant issues"} or
|
|
91
|
+
{"ok": false, "note": "<problem 1; problem 2; ...>"}."""
|
|
92
|
+
|
|
93
|
+
_ARCHITECTURE_PROMPT = """You are the ARCHITECTURE agent on an AI team working
|
|
94
|
+
inside CCT. You are handed the original request and the proposed
|
|
95
|
+
solution. Assess whether the structure (files/modules/components/API
|
|
96
|
+
shape) is sound, and name at most 2 concrete structural improvements.
|
|
97
|
+
Reply with ONLY a JSON object:
|
|
98
|
+
{"ok": true, "note": "structure is sound"} or
|
|
99
|
+
{"ok": false, "note": "<concrete improvements>"}."""
|
|
100
|
+
|
|
101
|
+
_UI_PROMPT = """You are the UI agent on an AI team working inside CCT. You
|
|
102
|
+
are handed the request and the proposed solution. Assess usability and
|
|
103
|
+
visual clarity at a high level; suggest at most 2 concrete improvements
|
|
104
|
+
(layout, hierarchy, affordances — not pixel details). Reply with ONLY a
|
|
105
|
+
JSON object:
|
|
106
|
+
{"ok": true, "note": "UI approach is sound"} or
|
|
107
|
+
{"ok": false, "note": "<concrete improvements>"}."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_json(text):
|
|
111
|
+
if not text:
|
|
112
|
+
return {}
|
|
113
|
+
try:
|
|
114
|
+
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
|
|
115
|
+
m = re.search(r"\{.*\}", text, re.S)
|
|
116
|
+
return json.loads(m.group(0)) if m else {}
|
|
117
|
+
except Exception:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _quick_judge(prompt_text, system_prompt):
|
|
122
|
+
"""One focused model pass that returns (ok, note) — the generic
|
|
123
|
+
reviewer-style check used by reviewer/performance/architecture/ui.
|
|
124
|
+
Never raises; a garbled response is treated as 'ok' (no fabricated
|
|
125
|
+
criticism) — same honesty rule agent.py's _verify_answer uses."""
|
|
126
|
+
try:
|
|
127
|
+
raw = aicore.query_ai(prompt_text, system_prompt=system_prompt)
|
|
128
|
+
data = _extract_json(raw)
|
|
129
|
+
if data.get("ok") is False and data.get("note"):
|
|
130
|
+
return False, str(data["note"])
|
|
131
|
+
return True, ""
|
|
132
|
+
except Exception:
|
|
133
|
+
return True, ""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_orchestrated(user_text, max_steps=agent.MAX_STEPS, on_agent=None,
|
|
137
|
+
mode="agent", permission_callback=None, turn_id=""):
|
|
138
|
+
"""Runs the Coordinator-led specialist pipeline. Returns
|
|
139
|
+
(final_text, steps, meta, agents) where agents is the monitored
|
|
140
|
+
roster (list of {key, label, status, summary})."""
|
|
141
|
+
|
|
142
|
+
agents = []
|
|
143
|
+
_act_ids = {} # key -> activity id for live activity tracking
|
|
144
|
+
|
|
145
|
+
def _report(key, status, summary=""):
|
|
146
|
+
label = SPECIALIST_ROLES[key][0]
|
|
147
|
+
agents.append({"key": key, "label": label, "status": status, "summary": summary})
|
|
148
|
+
if on_agent:
|
|
149
|
+
on_agent(key, label, status, summary)
|
|
150
|
+
# v0.8.0: Emit to event stream
|
|
151
|
+
try:
|
|
152
|
+
if status == "start":
|
|
153
|
+
event_stream.stream.emit("agent_started", source="orchestrator",
|
|
154
|
+
role=key, label=label)
|
|
155
|
+
elif status == "done":
|
|
156
|
+
event_stream.stream.emit("agent_completed", source="orchestrator",
|
|
157
|
+
role=key, label=label, summary=summary)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
def _run(key):
|
|
162
|
+
_report(key, "start")
|
|
163
|
+
sound.play("tool")
|
|
164
|
+
# v0.8.0: Create live Activity for this specialist agent
|
|
165
|
+
try:
|
|
166
|
+
from . import activity as _act
|
|
167
|
+
label = SPECIALIST_ROLES[key][0]
|
|
168
|
+
desc = SPECIALIST_ROLES[key][1]
|
|
169
|
+
act = _act.manager.create(
|
|
170
|
+
type=_act.TYPE_STATUS, action="agent",
|
|
171
|
+
title=f"Agent: {label}",
|
|
172
|
+
status=_act.RUNNING, turn_id=turn_id,
|
|
173
|
+
details=desc, description=f"{label} — {desc}"
|
|
174
|
+
)
|
|
175
|
+
_act_ids[key] = act.id
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
def _done(key, summary=""):
|
|
180
|
+
_report(key, "done", summary)
|
|
181
|
+
sound.play("success")
|
|
182
|
+
# v0.8.0: Complete the live Activity for this specialist
|
|
183
|
+
try:
|
|
184
|
+
from . import activity as _act
|
|
185
|
+
aid = _act_ids.get(key)
|
|
186
|
+
if aid:
|
|
187
|
+
_act.manager.update(aid, status=_act.COMPLETED, result=summary[:100] if summary else "Done")
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
system_prompt = agent.AGENT_SYSTEM_PROMPT if mode == "agent" else agent.AI_SYSTEM_PROMPT
|
|
192
|
+
|
|
193
|
+
# ---- Coordinator: build the roster -----------------------------------
|
|
194
|
+
wants_code = bool(_CODE_HINTS.search(user_text))
|
|
195
|
+
wants_ui = bool(_UI_HINTS.search(user_text))
|
|
196
|
+
wants_arch = bool(_ARCH_HINTS.search(user_text))
|
|
197
|
+
wants_perf = bool(_PERF_HINTS.search(user_text))
|
|
198
|
+
wants_research = bool(agent._RESEARCH_HINTS.search(user_text))
|
|
199
|
+
roster = ["planner"]
|
|
200
|
+
if wants_research:
|
|
201
|
+
roster.insert(0, "research")
|
|
202
|
+
roster.append("coding")
|
|
203
|
+
roster.append("reviewer")
|
|
204
|
+
if wants_ui:
|
|
205
|
+
roster.append("ui")
|
|
206
|
+
if wants_perf:
|
|
207
|
+
roster.append("performance")
|
|
208
|
+
if wants_arch:
|
|
209
|
+
roster.append("architecture")
|
|
210
|
+
if wants_code:
|
|
211
|
+
roster.extend(["security", "testing"])
|
|
212
|
+
roster.append("documentation")
|
|
213
|
+
_report("coordinator", "start",
|
|
214
|
+
f"roster: {', '.join(SPECIALIST_ROLES[r][0] for r in roster)}")
|
|
215
|
+
|
|
216
|
+
# ---- Planner -----------------------------------------------------------
|
|
217
|
+
_run("planner")
|
|
218
|
+
plan = agent._plan_steps(user_text, system_prompt)
|
|
219
|
+
_done("planner", f"{len(plan)} milestone(s)")
|
|
220
|
+
|
|
221
|
+
# ---- Research ----------------------------------------------------------
|
|
222
|
+
research_brief = ""
|
|
223
|
+
if wants_research:
|
|
224
|
+
_run("research")
|
|
225
|
+
research_brief = agent._research_pass(user_text, system_prompt)
|
|
226
|
+
_done("research", "brief folded in" if research_brief else "nothing relevant found")
|
|
227
|
+
|
|
228
|
+
# ---- Coding (the real tool loop) --------------------------------------
|
|
229
|
+
_run("coding")
|
|
230
|
+
plan_note = ("\n\nTEAM PLAN (from the Planner \u2014 follow it, adapt if needed):\n" +
|
|
231
|
+
"\n".join(f"{i+1}. {p}" for i, p in enumerate(plan)))
|
|
232
|
+
if research_brief:
|
|
233
|
+
plan_note += "\n\n" + research_brief
|
|
234
|
+
final_text, steps, meta = agent.run_agent(
|
|
235
|
+
user_text + plan_note, max_steps=max_steps, verbose=False, mode=mode,
|
|
236
|
+
permission_callback=permission_callback, turn_id=turn_id)
|
|
237
|
+
_done("coding", f"{len(steps)} tool step(s)")
|
|
238
|
+
|
|
239
|
+
# ---- Reviewer -----------------------------------------------------------
|
|
240
|
+
_run("reviewer")
|
|
241
|
+
ok, note = _quick_judge(
|
|
242
|
+
"ORIGINAL REQUEST:\n" + user_text +
|
|
243
|
+
"\n\nCODING AGENT'S ANSWER:\n" + final_text,
|
|
244
|
+
_REVIEWER_PROMPT)
|
|
245
|
+
if not ok and note:
|
|
246
|
+
final_text = final_text + f"\n\n\u2696 Reviewer flag: {note}"
|
|
247
|
+
_done("reviewer", "ok" if ok else f"flag: {note}")
|
|
248
|
+
|
|
249
|
+
# ---- UI / Performance / Architecture (content-gated) -------------------
|
|
250
|
+
for key, prompt, prompt_text in (
|
|
251
|
+
("ui", _UI_PROMPT, "REQUEST:\n" + user_text + "\n\nSOLUTION:\n" + final_text),
|
|
252
|
+
("performance", _PERFORMANCE_PROMPT, "GENERATED CODE/SOLUTION:\n" + final_text),
|
|
253
|
+
("architecture", _ARCHITECTURE_PROMPT,
|
|
254
|
+
"ORIGINAL REQUEST:\n" + user_text + "\n\nPROPOSED SOLUTION:\n" + final_text),
|
|
255
|
+
):
|
|
256
|
+
if key not in roster:
|
|
257
|
+
continue
|
|
258
|
+
_run(key)
|
|
259
|
+
p_ok, p_note = _quick_judge(prompt_text, prompt)
|
|
260
|
+
if not p_ok and p_note:
|
|
261
|
+
final_text = final_text + f"\n\n\u26a0 {SPECIALIST_ROLES[key][0]} note: {p_note}"
|
|
262
|
+
_done(key, "ok" if p_ok else f"note: {p_note}")
|
|
263
|
+
|
|
264
|
+
# ---- Security + Testing (only for coding requests) ---------------------
|
|
265
|
+
if wants_code:
|
|
266
|
+
_run("security")
|
|
267
|
+
code = agent._extract_code_block(final_text)
|
|
268
|
+
findings = security_scanner.scan_code(code) if code else []
|
|
269
|
+
if findings:
|
|
270
|
+
final_text += agent._DEBUGGER_NOTE_TEMPLATE.format(
|
|
271
|
+
summary=security_scanner.summarize(findings),
|
|
272
|
+
details="\n".join(f" {repr(f)}" for f in findings[:5]))
|
|
273
|
+
_done("security",
|
|
274
|
+
f"{len(findings)} finding(s)" if findings else "no risky patterns")
|
|
275
|
+
|
|
276
|
+
_run("testing")
|
|
277
|
+
final_text, debug_note = agent._debug_pass(final_text)
|
|
278
|
+
if debug_note:
|
|
279
|
+
sb = debug_note.get("sandbox_test")
|
|
280
|
+
_done("testing", f"risk {debug_note.get('risk')}" +
|
|
281
|
+
(f" \u00b7 sandbox exit {sb.get('returncode')}" if isinstance(sb, dict) else ""))
|
|
282
|
+
else:
|
|
283
|
+
_done("testing", "no code block to test")
|
|
284
|
+
else:
|
|
285
|
+
_run("testing")
|
|
286
|
+
ok_t, note_t = agent._verify_answer(user_text, plan, final_text, system_prompt)
|
|
287
|
+
if not ok_t and note_t:
|
|
288
|
+
final_text = final_text + f"\n\n\u26a0 Tester flagged this for review: {note_t}"
|
|
289
|
+
_done("testing", "ok" if ok_t else f"flag: {note_t}")
|
|
290
|
+
|
|
291
|
+
# ---- Documentation ------------------------------------------------------
|
|
292
|
+
_run("documentation")
|
|
293
|
+
doc = ("\u2705 Orchestrated run complete\n\n"
|
|
294
|
+
"Agents that worked on this:\n" +
|
|
295
|
+
"\n".join(f"- {SPECIALIST_ROLES[a['key']][0]} \u2014 {a['status']}"
|
|
296
|
+
+ (f" ({a['summary']})" if a.get("summary") else "")
|
|
297
|
+
for a in agents if a["key"] != "coordinator"))
|
|
298
|
+
final_text = final_text.rstrip() + "\n\n" + doc
|
|
299
|
+
_done("documentation", "final report appended")
|
|
300
|
+
|
|
301
|
+
_report("coordinator", "done", f"{len(agents)} agent events monitored")
|
|
302
|
+
meta = dict(meta or {})
|
|
303
|
+
meta["roster"] = [a["key"] for a in agents]
|
|
304
|
+
return final_text, steps, meta, agents
|