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,723 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT — collaboration.py: Multi-AI Collaboration Engine.
|
|
3
|
+
|
|
4
|
+
Extends the single-orchestrator pattern (orchestrator.py) into a richer
|
|
5
|
+
multi-agent collaboration framework with:
|
|
6
|
+
|
|
7
|
+
- A shared CollaborationContext that every agent reads from and writes to.
|
|
8
|
+
- Named agent roles, each with a dedicated system prompt.
|
|
9
|
+
- File-lock coordination so two agents never edit the same file at once.
|
|
10
|
+
- A MultiAgentOrchestrator that sequences roles, feeds the real tool
|
|
11
|
+
system (agent.TOOLS) to the coding agent, and collects structured
|
|
12
|
+
findings from every stage.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from .collaboration import MultiAgentOrchestrator, CollaborationContext
|
|
16
|
+
|
|
17
|
+
orch = MultiAgentOrchestrator(query_fn=aicore.query_ai, tools_dict=agent.TOOLS)
|
|
18
|
+
final_text, steps, meta, agents = orch.run_collaborative("Plot the Arrhenius curve for ...")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
print("This is a library file and is not meant to be run directly.")
|
|
23
|
+
import sys
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
33
|
+
|
|
34
|
+
from . import aicore
|
|
35
|
+
from . import tool_call_normalizer
|
|
36
|
+
from . import agent
|
|
37
|
+
from . import security_scanner
|
|
38
|
+
from . import sandbox
|
|
39
|
+
|
|
40
|
+
# =====================================================================
|
|
41
|
+
# Agent Role Constants
|
|
42
|
+
# =====================================================================
|
|
43
|
+
|
|
44
|
+
PLANNER = "planner"
|
|
45
|
+
RESEARCHER = "researcher"
|
|
46
|
+
ARCHITECT = "architect"
|
|
47
|
+
CODER = "coder"
|
|
48
|
+
UI_DESIGNER = "ui_designer"
|
|
49
|
+
DEBUGGER = "debugger"
|
|
50
|
+
REVIEWER = "reviewer"
|
|
51
|
+
TESTER = "tester"
|
|
52
|
+
SECURITY = "security"
|
|
53
|
+
FINALIZER = "finalizer"
|
|
54
|
+
|
|
55
|
+
# =====================================================================
|
|
56
|
+
# Agent Role System Prompts
|
|
57
|
+
# =====================================================================
|
|
58
|
+
|
|
59
|
+
ROLE_PROMPTS: Dict[str, str] = {
|
|
60
|
+
PLANNER: (
|
|
61
|
+
"You are the PLANNER on a multi-AI team working inside CAT "
|
|
62
|
+
"(Chemistry Calc Terminal). You break a user request into a concise, "
|
|
63
|
+
"ordered list of concrete subtasks that other agents can execute "
|
|
64
|
+
"independently. You do NOT solve anything or call tools yourself.\n"
|
|
65
|
+
"Reply with ONLY a JSON object:\n"
|
|
66
|
+
'{"plan": ["subtask 1", "subtask 2", ...]}\n'
|
|
67
|
+
"Keep it to 2-8 actionable steps. No prose outside the JSON."
|
|
68
|
+
),
|
|
69
|
+
RESEARCHER: (
|
|
70
|
+
"You are the RESEARCHER on a multi-AI team working inside CCT. You "
|
|
71
|
+
"gather external context via live web search so other agents do not "
|
|
72
|
+
"reason from stale training data on fast-moving facts.\n"
|
|
73
|
+
"You receive the original request and any prior findings. Produce a "
|
|
74
|
+
"short, cited research brief — bullet points with sources — that the "
|
|
75
|
+
"team can fold into their work. If nothing relevant is found, say so "
|
|
76
|
+
"plainly. No fabrication."
|
|
77
|
+
),
|
|
78
|
+
ARCHITECT: (
|
|
79
|
+
"You are the ARCHITECT on a multi-AI team working inside CCT. You "
|
|
80
|
+
"review the structural design of proposed solutions: module layout, "
|
|
81
|
+
"file organisation, API boundaries, data flow.\n"
|
|
82
|
+
"You receive the original request and the current solution. Reply "
|
|
83
|
+
"with ONLY a JSON object:\n"
|
|
84
|
+
'{"ok": true/false, "note": "<concrete structural observation or '
|
|
85
|
+
'improvement suggestion, one sentence>"}'
|
|
86
|
+
),
|
|
87
|
+
CODER: (
|
|
88
|
+
"You are the CODER on a multi-AI team working inside CCT. You have "
|
|
89
|
+
"access to real tools (solve formulas, plot, write files, run "
|
|
90
|
+
"commands, etc.). Use them — never fabricate results that could be "
|
|
91
|
+
"computed.\n"
|
|
92
|
+
"Follow the plan handed to you. When you are finished, return a "
|
|
93
|
+
"comprehensive answer using the standard CAT notebook format."
|
|
94
|
+
),
|
|
95
|
+
UI_DESIGNER: (
|
|
96
|
+
"You are the UI_DESIGNER on a multi-AI team working inside CCT. You "
|
|
97
|
+
"evaluate the usability, layout, and visual clarity of UI-facing "
|
|
98
|
+
"proposals.\n"
|
|
99
|
+
"Reply with ONLY a JSON object:\n"
|
|
100
|
+
'{"ok": true/false, "note": "<concrete UI improvement or approval>"}'
|
|
101
|
+
),
|
|
102
|
+
DEBUGGER: (
|
|
103
|
+
"You are the DEBUGGER on a multi-AI team working inside CCT. You "
|
|
104
|
+
"review generated code for correctness, edge cases, and runtime "
|
|
105
|
+
"errors. You may also run sandboxed tests.\n"
|
|
106
|
+
"Reply with ONLY a JSON object:\n"
|
|
107
|
+
'{"ok": true/false, "note": "<issue description or \'no issues\'>"}'
|
|
108
|
+
),
|
|
109
|
+
REVIEWER: (
|
|
110
|
+
"You are the REVIEWER on a multi-AI team working inside CCT. You "
|
|
111
|
+
"cross-check the CODER's final output against the original request. "
|
|
112
|
+
"Look for unanswered parts, implied assumptions, and inconsistencies.\n"
|
|
113
|
+
"Reply with ONLY a JSON object:\n"
|
|
114
|
+
'{"ok": true/false, "note": "<what is missing or wrong, one sentence>"}'
|
|
115
|
+
),
|
|
116
|
+
TESTER: (
|
|
117
|
+
"You are the TESTER on a multi-AI team working inside CCT. You "
|
|
118
|
+
"spot-check arithmetic, units, and internal consistency of the "
|
|
119
|
+
"answer against the given data.\n"
|
|
120
|
+
"Reply with ONLY a JSON object:\n"
|
|
121
|
+
'{"ok": true/false, "note": "<what looks wrong, one sentence>"}'
|
|
122
|
+
),
|
|
123
|
+
SECURITY: (
|
|
124
|
+
"You are the SECURITY agent on a multi-AI team working inside CCT. "
|
|
125
|
+
"You run a static security scan on any generated code and flag "
|
|
126
|
+
"dangerous patterns (subprocess calls, eval, exec, file-system "
|
|
127
|
+
"deletion, credential leaks, network calls, etc.).\n"
|
|
128
|
+
"Reply with ONLY a JSON object:\n"
|
|
129
|
+
'{"ok": true/false, "note": "<findings summary or \'clean\'>"}'
|
|
130
|
+
),
|
|
131
|
+
FINALIZER: (
|
|
132
|
+
"You are the FINALIZER on a multi-AI team working inside CCT. You "
|
|
133
|
+
"receive all prior agent findings, decisions, and the coder's output. "
|
|
134
|
+
"Your job is to produce the single polished final answer the user "
|
|
135
|
+
"will see — merging feedback, resolving conflicts, and ensuring "
|
|
136
|
+
"completeness.\n"
|
|
137
|
+
"Reply with ONLY the final answer text, no JSON wrapper."
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# =====================================================================
|
|
142
|
+
# Content-gate patterns (which roles to activate for which task types)
|
|
143
|
+
# =====================================================================
|
|
144
|
+
|
|
145
|
+
_CODE_HINTS = re.compile(
|
|
146
|
+
r"\b(code|python|script|program|function|write a .*(class|module)|"
|
|
147
|
+
r"generate.*(code|script))\b", re.I)
|
|
148
|
+
_UI_HINTS = re.compile(
|
|
149
|
+
r"\b(ui|interface|screen|layout|widget|button|menu|dashboard|"
|
|
150
|
+
r"frontend|web page|landing page|design)\b", re.I)
|
|
151
|
+
_ARCH_HINTS = re.compile(
|
|
152
|
+
r"\b(architecture|design|structure|module|layers?|service|"
|
|
153
|
+
r"microservice|api design|database schema|data model)\b", re.I)
|
|
154
|
+
_RESEARCH_HINTS = re.compile(
|
|
155
|
+
r"\b(latest|recent|current|news|today|this year|search|look up|find out|"
|
|
156
|
+
r"who is|what happened|price of|update on)\b", re.I)
|
|
157
|
+
|
|
158
|
+
# =====================================================================
|
|
159
|
+
# CollaborationContext — shared mutable state passed between agents
|
|
160
|
+
# =====================================================================
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class CollaborationContext:
|
|
164
|
+
"""Mutable state bag shared across all agents during a collaborative run.
|
|
165
|
+
|
|
166
|
+
Every agent reads from and appends to the fields it cares about. The
|
|
167
|
+
orchestrator is responsible for serialising this context into each
|
|
168
|
+
agent's prompt so they can see what prior agents decided / found.
|
|
169
|
+
"""
|
|
170
|
+
task: str = ""
|
|
171
|
+
project_state: Dict[str, Any] = field(default_factory=dict)
|
|
172
|
+
relevant_files: List[str] = field(default_factory=list)
|
|
173
|
+
agent_findings: Dict[str, Any] = field(default_factory=dict)
|
|
174
|
+
decisions: List[str] = field(default_factory=list)
|
|
175
|
+
issues: List[str] = field(default_factory=list)
|
|
176
|
+
tool_results: List[Dict[str, Any]] = field(default_factory=list)
|
|
177
|
+
open_questions: List[str] = field(default_factory=list)
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
# serialisation helpers
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
def to_prompt_block(self, exclude_roles: Optional[List[str]] = None) -> str:
|
|
183
|
+
"""Render the context as a short text block suitable for injection
|
|
184
|
+
into a system prompt so an agent can see what came before it."""
|
|
185
|
+
exclude = set(exclude_roles or [])
|
|
186
|
+
lines: List[str] = []
|
|
187
|
+
if self.task:
|
|
188
|
+
lines.append(f"TASK: {self.task}")
|
|
189
|
+
if self.decisions:
|
|
190
|
+
lines.append("DECISIONS:")
|
|
191
|
+
for d in self.decisions:
|
|
192
|
+
lines.append(f" - {d}")
|
|
193
|
+
if self.issues:
|
|
194
|
+
lines.append("OPEN ISSUES:")
|
|
195
|
+
for i in self.issues:
|
|
196
|
+
lines.append(f" - {i}")
|
|
197
|
+
if self.open_questions:
|
|
198
|
+
lines.append("OPEN QUESTIONS:")
|
|
199
|
+
for q in self.open_questions:
|
|
200
|
+
lines.append(f" - {q}")
|
|
201
|
+
if self.relevant_files:
|
|
202
|
+
lines.append("RELEVANT FILES: " + ", ".join(self.relevant_files))
|
|
203
|
+
for role_key, finding in self.agent_findings.items():
|
|
204
|
+
if role_key in exclude:
|
|
205
|
+
continue
|
|
206
|
+
label = role_key.replace("_", " ").title()
|
|
207
|
+
lines.append(f"{label.upper()} FINDING: {finding}")
|
|
208
|
+
return "\n".join(lines)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# =====================================================================
|
|
212
|
+
# FileLockManager — prevent concurrent edits to the same file
|
|
213
|
+
# =====================================================================
|
|
214
|
+
|
|
215
|
+
class FileLockManager:
|
|
216
|
+
"""Lightweight in-process file lock.
|
|
217
|
+
|
|
218
|
+
Agents request an exclusive lock before editing a file and release it
|
|
219
|
+
when done. If another agent already holds the lock the request fails
|
|
220
|
+
(returns False) so the orchestrator can retry or skip.
|
|
221
|
+
|
|
222
|
+
Thread-safe via a lock on the internal dict.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(self) -> None:
|
|
226
|
+
self._locked: Dict[str, str] = {} # file_path -> agent_id
|
|
227
|
+
self._lock = threading.Lock()
|
|
228
|
+
|
|
229
|
+
def lock(self, file_path: str, agent_id: str) -> bool:
|
|
230
|
+
"""Attempt to acquire an exclusive lock for *file_path*.
|
|
231
|
+
|
|
232
|
+
Returns True on success, False if another agent already holds it.
|
|
233
|
+
"""
|
|
234
|
+
with self._lock:
|
|
235
|
+
if file_path in self._locked:
|
|
236
|
+
return False
|
|
237
|
+
self._locked[file_path] = agent_id
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
def unlock(self, file_path: str, agent_id: str) -> None:
|
|
241
|
+
"""Release the lock on *file_path* if *agent_id* is the current owner.
|
|
242
|
+
|
|
243
|
+
Safe to call even if the lock was already released.
|
|
244
|
+
"""
|
|
245
|
+
with self._lock:
|
|
246
|
+
if self._locked.get(file_path) == agent_id:
|
|
247
|
+
del self._locked[file_path]
|
|
248
|
+
|
|
249
|
+
def is_locked(self, file_path: str) -> bool:
|
|
250
|
+
"""Return True if *file_path* is currently locked by any agent."""
|
|
251
|
+
with self._lock:
|
|
252
|
+
return file_path in self._locked
|
|
253
|
+
|
|
254
|
+
def get_lock_owner(self, file_path: str) -> Optional[str]:
|
|
255
|
+
"""Return the agent_id that holds the lock, or None."""
|
|
256
|
+
with self._lock:
|
|
257
|
+
return self._locked.get(file_path)
|
|
258
|
+
|
|
259
|
+
def release_all(self, agent_id: str) -> List[str]:
|
|
260
|
+
"""Release every lock held by *agent_id*. Returns list of paths released."""
|
|
261
|
+
released: List[str] = []
|
|
262
|
+
with self._lock:
|
|
263
|
+
for path in list(self._locked):
|
|
264
|
+
if self._locked[path] == agent_id:
|
|
265
|
+
del self._locked[path]
|
|
266
|
+
released.append(path)
|
|
267
|
+
return released
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =====================================================================
|
|
271
|
+
# MultiAgentOrchestrator
|
|
272
|
+
# =====================================================================
|
|
273
|
+
|
|
274
|
+
def _extract_json(text: str) -> Dict[str, Any]:
|
|
275
|
+
"""Best-effort extraction of the first JSON object from *text*."""
|
|
276
|
+
if not text:
|
|
277
|
+
return {}
|
|
278
|
+
try:
|
|
279
|
+
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
|
|
280
|
+
m = re.search(r"\{.*\}", text, re.S)
|
|
281
|
+
return json.loads(m.group(0)) if m else {}
|
|
282
|
+
except Exception:
|
|
283
|
+
return {}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class MultiAgentOrchestrator:
|
|
287
|
+
"""Sequences specialist AI agents through a shared CollaborationContext.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
query_fn : callable
|
|
292
|
+
The AI query function — ``aicore.query_ai`` by default. Signature:
|
|
293
|
+
``query_fn(prompt, system_prompt=...) -> str``.
|
|
294
|
+
tools_dict : dict
|
|
295
|
+
The tools dictionary — ``agent.TOOLS`` by default. Only the CODER
|
|
296
|
+
role receives this directly; other roles see a filtered tool list
|
|
297
|
+
or no tools at all.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, query_fn: Optional[Callable] = None,
|
|
301
|
+
tools_dict: Optional[Dict[str, Any]] = None) -> None:
|
|
302
|
+
self.query_fn = query_fn or aicore.query_ai
|
|
303
|
+
self.tools_dict = tools_dict or agent.TOOLS
|
|
304
|
+
self.file_locks = FileLockManager()
|
|
305
|
+
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
# Internal helpers
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def _query(self, prompt: str, system_prompt: str) -> str:
|
|
311
|
+
"""Thin wrapper so all AI calls go through one choke-point."""
|
|
312
|
+
try:
|
|
313
|
+
raw = self.query_fn(prompt, system_prompt=system_prompt)
|
|
314
|
+
return raw or ""
|
|
315
|
+
except Exception:
|
|
316
|
+
return ""
|
|
317
|
+
|
|
318
|
+
def _extract_code_block(self, text: str) -> Optional[str]:
|
|
319
|
+
"""Pull the first fenced Python code block out of *text*."""
|
|
320
|
+
m = re.search(r"```(?:python)?\s*\n(.*?)```", text or "", re.S)
|
|
321
|
+
return m.group(1) if m else None
|
|
322
|
+
|
|
323
|
+
def _activate_roles(self, user_text: str,
|
|
324
|
+
roles: Optional[List[str]] = None) -> List[str]:
|
|
325
|
+
"""Determine which roles to activate based on task content and the
|
|
326
|
+
explicit *roles* override.
|
|
327
|
+
|
|
328
|
+
v0.7.9.0 (requirement #21 — DYNAMIC TEAM): the roster size now
|
|
329
|
+
follows real task complexity instead of always fielding everyone.
|
|
330
|
+
The model_router's complexity score decides:
|
|
331
|
+
simple (<= 0.35) → planner + coder + finalizer
|
|
332
|
+
moderate (<= 0.6) → + reviewer + tester
|
|
333
|
+
complex (> 0.6) → full gated roster (architect/ui/security/
|
|
334
|
+
debugger/researcher as content warrants)
|
|
335
|
+
"""
|
|
336
|
+
if roles is not None:
|
|
337
|
+
return list(roles)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
from . import model_router as _mr
|
|
341
|
+
types = _mr.classify(user_text)
|
|
342
|
+
complexity = _mr.complexity_score(types, user_text)
|
|
343
|
+
except Exception:
|
|
344
|
+
types, complexity = set(), 0.4
|
|
345
|
+
|
|
346
|
+
roster: List[Dict[str, str]] = [PLANNER, CODER]
|
|
347
|
+
wants_code = bool(_CODE_HINTS.search(user_text))
|
|
348
|
+
wants_ui = bool(_UI_HINTS.search(user_text))
|
|
349
|
+
wants_arch = bool(_ARCH_HINTS.search(user_text))
|
|
350
|
+
wants_research = bool(_RESEARCH_HINTS.search(user_text))
|
|
351
|
+
|
|
352
|
+
if complexity <= 0.35:
|
|
353
|
+
# Simple task: a second opinion is latency theatre — skip it.
|
|
354
|
+
pass
|
|
355
|
+
elif complexity <= 0.6:
|
|
356
|
+
roster.extend([REVIEWER, TESTER])
|
|
357
|
+
else:
|
|
358
|
+
if wants_research:
|
|
359
|
+
roster.append(RESEARCHER)
|
|
360
|
+
if wants_arch:
|
|
361
|
+
roster.append(ARCHITECT)
|
|
362
|
+
if wants_ui:
|
|
363
|
+
roster.append(UI_DESIGNER)
|
|
364
|
+
roster.append(REVIEWER)
|
|
365
|
+
if wants_code:
|
|
366
|
+
roster.extend([DEBUGGER, SECURITY])
|
|
367
|
+
roster.append(TESTER)
|
|
368
|
+
|
|
369
|
+
roster.append(FINALIZER)
|
|
370
|
+
# De-duplicate preserving order.
|
|
371
|
+
seen: set = set()
|
|
372
|
+
return [r for r in roster if not (r in seen or seen.add(r))]
|
|
373
|
+
|
|
374
|
+
def _run_single_role(self, role: str, ctx: CollaborationContext,
|
|
375
|
+
user_text: str, plan: List[str],
|
|
376
|
+
coder_result: str, steps: List,
|
|
377
|
+
permission_callback: Optional[Callable] = None,
|
|
378
|
+
on_agent: Optional[Callable] = None,
|
|
379
|
+
record_finding: bool = True) -> str:
|
|
380
|
+
"""Run one non-CODER specialist role and return its output string.
|
|
381
|
+
|
|
382
|
+
v0.7.9.0 fixes:
|
|
383
|
+
* The output is now actually recorded into
|
|
384
|
+
`ctx.agent_findings[role]` — previously the planner's plan was
|
|
385
|
+
written nowhere, so run_collaborative always read an empty
|
|
386
|
+
finding and silently fell back to a generic one-step plan.
|
|
387
|
+
* RESEARCHER performs a REAL web search (aicore.web_search) and
|
|
388
|
+
hands the genuine snippets to the model, instead of pretending.
|
|
389
|
+
"""
|
|
390
|
+
role_label = role.replace("_", " ").title()
|
|
391
|
+
if on_agent:
|
|
392
|
+
on_agent(role, role_label, "start", "")
|
|
393
|
+
|
|
394
|
+
ctx_block = ctx.to_prompt_block(exclude_roles=[role])
|
|
395
|
+
prompt_parts: List[str] = []
|
|
396
|
+
|
|
397
|
+
# Build role-specific prompt
|
|
398
|
+
if role == PLANNER:
|
|
399
|
+
prompt_parts.append(f"User request:\n{user_text}")
|
|
400
|
+
elif role == RESEARCHER:
|
|
401
|
+
prompt_parts.append(f"User request:\n{user_text}")
|
|
402
|
+
prompt_parts.append(f"Context:\n{ctx_block}")
|
|
403
|
+
elif role == ARCHITECT:
|
|
404
|
+
prompt_parts.append(f"ORIGINAL REQUEST:\n{user_text}")
|
|
405
|
+
prompt_parts.append(f"CURRENT SOLUTION:\n{coder_result or '(not yet produced)'}")
|
|
406
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
407
|
+
elif role == UI_DESIGNER:
|
|
408
|
+
prompt_parts.append(f"ORIGINAL REQUEST:\n{user_text}")
|
|
409
|
+
prompt_parts.append(f"CURRENT SOLUTION:\n{coder_result}")
|
|
410
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
411
|
+
elif role == REVIEWER:
|
|
412
|
+
prompt_parts.append(f"ORIGINAL REQUEST:\n{user_text}")
|
|
413
|
+
prompt_parts.append(f"PLAN:\n" + "\n".join(f"- {p}" for p in plan))
|
|
414
|
+
prompt_parts.append(f"CODER OUTPUT:\n{coder_result}")
|
|
415
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
416
|
+
elif role == TESTER:
|
|
417
|
+
prompt_parts.append(f"ORIGINAL REQUEST:\n{user_text}")
|
|
418
|
+
prompt_parts.append(f"PLAN:\n" + "\n".join(f"- {p}" for p in plan))
|
|
419
|
+
prompt_parts.append(f"CODER OUTPUT:\n{coder_result}")
|
|
420
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
421
|
+
elif role == DEBUGGER:
|
|
422
|
+
prompt_parts.append(f"CODE TO REVIEW:\n{coder_result}")
|
|
423
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
424
|
+
elif role == SECURITY:
|
|
425
|
+
prompt_parts.append(f"CODE TO SCAN:\n{coder_result}")
|
|
426
|
+
prompt_parts.append(f"CONTEXT:\n{ctx_block}")
|
|
427
|
+
elif role == FINALIZER:
|
|
428
|
+
prompt_parts.append(f"ORIGINAL REQUEST:\n{user_text}")
|
|
429
|
+
prompt_parts.append(f"PLAN:\n" + "\n".join(f"- {p}" for p in plan))
|
|
430
|
+
prompt_parts.append(f"CODER OUTPUT:\n{coder_result}")
|
|
431
|
+
prompt_parts.append(f"ALL FINDINGS:\n{ctx_block}")
|
|
432
|
+
else:
|
|
433
|
+
prompt_parts.append(f"User request:\n{user_text}")
|
|
434
|
+
prompt_parts.append(f"Context:\n{ctx_block}")
|
|
435
|
+
|
|
436
|
+
prompt = "\n\n".join(prompt_parts)
|
|
437
|
+
system = ROLE_PROMPTS.get(role, ROLE_PROMPTS[PLANNER])
|
|
438
|
+
|
|
439
|
+
# RESEARCHER: real web retrieval first; the model then synthesizes
|
|
440
|
+
# over the ACTUAL results (never fabricated sources).
|
|
441
|
+
if role == RESEARCHER:
|
|
442
|
+
try:
|
|
443
|
+
results = aicore.web_search(user_text, max_results=5) or []
|
|
444
|
+
except Exception:
|
|
445
|
+
results = []
|
|
446
|
+
if results:
|
|
447
|
+
evidence = "\n\n".join(
|
|
448
|
+
f"[{i + 1}] {r['title']}\n{r['url']}\n{r['snippet']}"
|
|
449
|
+
for i, r in enumerate(results))
|
|
450
|
+
prompt += ("\n\nLIVE SEARCH RESULTS (use ONLY these as external "
|
|
451
|
+
"facts, cite by bracket number):\n" + evidence)
|
|
452
|
+
system += " Cite the bracketed result numbers you used."
|
|
453
|
+
else:
|
|
454
|
+
prompt += ("\n\nLIVE SEARCH RESULTS: none retrieved (search "
|
|
455
|
+
"unreachable). Say so plainly — do not invent sources.")
|
|
456
|
+
system += " No web results were retrievable; say so plainly."
|
|
457
|
+
|
|
458
|
+
output = self._query(prompt, system)
|
|
459
|
+
|
|
460
|
+
# Parse structured JSON responses where expected
|
|
461
|
+
if role in (PLANNER, REVIEWER, TESTER, UI_DESIGNER, ARCHITECT,
|
|
462
|
+
DEBUGGER, SECURITY):
|
|
463
|
+
data = _extract_json(output)
|
|
464
|
+
if role == PLANNER:
|
|
465
|
+
plan_data = data.get("plan")
|
|
466
|
+
if isinstance(plan_data, list) and plan_data:
|
|
467
|
+
output = json.dumps({"plan": [str(s) for s in plan_data[:8]]})
|
|
468
|
+
# For review-type roles, store as-is; orchestrator decides what to do
|
|
469
|
+
|
|
470
|
+
# THE FIX: record the finding so later roles (and the orchestrator)
|
|
471
|
+
# can actually see this agent's work via the shared context.
|
|
472
|
+
if record_finding:
|
|
473
|
+
try:
|
|
474
|
+
ctx.agent_findings[role] = output
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
if on_agent:
|
|
479
|
+
summary = output[:120].replace("\n", " ") if output else "(empty)"
|
|
480
|
+
on_agent(role, role_label, "done", summary)
|
|
481
|
+
|
|
482
|
+
return output
|
|
483
|
+
|
|
484
|
+
# ------------------------------------------------------------------
|
|
485
|
+
# File-locking integration for the CODER role
|
|
486
|
+
# ------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
def _acquire_file_locks(self, coder_result: str, coder_id: str = "coder") -> None:
|
|
489
|
+
"""Scan coder output for write_file / create_folder tool calls and
|
|
490
|
+
acquire file locks for the paths they touch."""
|
|
491
|
+
for tr in coder_result if isinstance(coder_result, list) else []:
|
|
492
|
+
if isinstance(tr, dict):
|
|
493
|
+
tool_name = tr.get("tool", "")
|
|
494
|
+
args = tr.get("args", {})
|
|
495
|
+
path = args.get("path", "")
|
|
496
|
+
if tool_name in ("write_file", "rename_file", "delete_file") and path:
|
|
497
|
+
self.file_locks.lock(path, coder_id)
|
|
498
|
+
elif tool_name == "create_folder" and path:
|
|
499
|
+
self.file_locks.lock(path, coder_id)
|
|
500
|
+
|
|
501
|
+
def _release_file_locks(self, coder_id: str = "coder") -> None:
|
|
502
|
+
"""Release all locks held by the coder agent."""
|
|
503
|
+
self.file_locks.release_all(coder_id)
|
|
504
|
+
|
|
505
|
+
# ------------------------------------------------------------------
|
|
506
|
+
# Main collaborative pipeline
|
|
507
|
+
# ------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
def run_collaborative(
|
|
510
|
+
self,
|
|
511
|
+
user_text: str,
|
|
512
|
+
roles: Optional[List[str]] = None,
|
|
513
|
+
on_agent: Optional[Callable] = None,
|
|
514
|
+
permission_callback: Optional[Callable] = None,
|
|
515
|
+
max_steps: int = 12,
|
|
516
|
+
) -> Tuple[str, list, dict, list]:
|
|
517
|
+
"""Run the full multi-agent collaborative pipeline.
|
|
518
|
+
|
|
519
|
+
Parameters
|
|
520
|
+
----------
|
|
521
|
+
user_text : str
|
|
522
|
+
The user's original request.
|
|
523
|
+
roles : list[str] | None
|
|
524
|
+
Explicit roster of role keys to activate. When *None* the
|
|
525
|
+
roster is chosen automatically based on task content.
|
|
526
|
+
on_agent : callable | None
|
|
527
|
+
``on_agent(role_key, role_label, status, summary)`` — called
|
|
528
|
+
on every agent start/done event so the UI can show a live
|
|
529
|
+
roster. *status* is ``"start"`` or ``"done"``.
|
|
530
|
+
permission_callback : callable | None
|
|
531
|
+
Forwarded to the CODER's ``agent.run_agent`` call for
|
|
532
|
+
write-permission gates.
|
|
533
|
+
max_steps : int
|
|
534
|
+
Maximum tool-loop steps for the CODER agent.
|
|
535
|
+
|
|
536
|
+
Returns
|
|
537
|
+
-------
|
|
538
|
+
(final_text, steps, meta, agents)
|
|
539
|
+
final_text : str — the merged, polished answer.
|
|
540
|
+
steps : list — tool-call steps from the CODER agent.
|
|
541
|
+
meta : dict — pipeline metadata (roster, plan, etc.).
|
|
542
|
+
agents : list[dict] — ``{key, label, status, summary}`` for
|
|
543
|
+
every agent event (chronological).
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
agents: List[Dict[str, str]] = []
|
|
547
|
+
steps: List[Any] = []
|
|
548
|
+
ctx = CollaborationContext(task=user_text)
|
|
549
|
+
|
|
550
|
+
def _report(key: str, status: str, summary: str = "") -> None:
|
|
551
|
+
label = key.replace("_", " ").title()
|
|
552
|
+
agents.append({"key": key, "label": label, "status": status, "summary": summary})
|
|
553
|
+
if on_agent:
|
|
554
|
+
on_agent(key, label, status, summary)
|
|
555
|
+
try:
|
|
556
|
+
from .event_stream import stream as _es, MULTI_AGENT_PROGRESS
|
|
557
|
+
_es.emit(MULTI_AGENT_PROGRESS, source="multi_ai",
|
|
558
|
+
agent=key, status=status, summary=str(summary)[:160])
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
from .event_stream import stream as _es, MULTI_AGENT_STARTED
|
|
564
|
+
roster_preview = self._activate_roles(user_text, roles)
|
|
565
|
+
_es.emit(MULTI_AGENT_STARTED, source="multi_ai",
|
|
566
|
+
roster=roster_preview, task=user_text[:200])
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
# ---- 1. Determine which roles to activate --------------------
|
|
571
|
+
roster = self._activate_roles(user_text, roles)
|
|
572
|
+
_report("orchestrator", "start",
|
|
573
|
+
f"roster: {', '.join(r.replace('_', ' ').title() for r in roster)}")
|
|
574
|
+
|
|
575
|
+
# ---- 2. PLANNER — break task into subtasks -------------------
|
|
576
|
+
plan: List[str] = []
|
|
577
|
+
if PLANNER in roster:
|
|
578
|
+
self._run_single_role(PLANNER, ctx, user_text, [], "", steps,
|
|
579
|
+
permission_callback, on_agent)
|
|
580
|
+
# Parse plan from the planner's REAL recorded finding
|
|
581
|
+
# (ctx.agent_findings is now actually written — see
|
|
582
|
+
# _run_single_role's v0.7.9.0 fix).
|
|
583
|
+
planner_findings = ctx.agent_findings.get(PLANNER, "")
|
|
584
|
+
data = _extract_json(planner_findings)
|
|
585
|
+
plan_raw = data.get("plan")
|
|
586
|
+
if isinstance(plan_raw, list) and plan_raw:
|
|
587
|
+
plan = [str(s) for s in plan_raw[:8]]
|
|
588
|
+
else:
|
|
589
|
+
plan = ["Work out and answer the question directly."]
|
|
590
|
+
ctx.decisions.append(f"Plan: {len(plan)} subtasks defined")
|
|
591
|
+
else:
|
|
592
|
+
plan = ["Work out and answer the question directly."]
|
|
593
|
+
|
|
594
|
+
# ---- 3. RESEARCHER — real web search for context ------------------
|
|
595
|
+
research_brief = ""
|
|
596
|
+
if RESEARCHER in roster:
|
|
597
|
+
research_brief = self._run_single_role(
|
|
598
|
+
RESEARCHER, ctx, user_text, plan, "", steps,
|
|
599
|
+
permission_callback, on_agent)
|
|
600
|
+
if research_brief:
|
|
601
|
+
ctx.decisions.append("Research brief obtained")
|
|
602
|
+
|
|
603
|
+
# ---- 4. CODER — run the real tool loop -----------------------
|
|
604
|
+
coder_result = ""
|
|
605
|
+
coder_steps: list = []
|
|
606
|
+
if CODER in roster:
|
|
607
|
+
_report(CODER, "start", "running tool loop")
|
|
608
|
+
plan_note = "\n\nTEAM PLAN (from the Planner — follow it, adapt if needed):\n" + \
|
|
609
|
+
"\n".join(f"{i+1}. {p}" for i, p in enumerate(plan))
|
|
610
|
+
if research_brief:
|
|
611
|
+
plan_note += "\n\nRESEARCH BRIEF:\n" + research_brief
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
coder_result, coder_steps, coder_meta = agent.run_agent(
|
|
615
|
+
user_text + plan_note,
|
|
616
|
+
max_steps=max_steps,
|
|
617
|
+
verbose=False,
|
|
618
|
+
mode="agent",
|
|
619
|
+
permission_callback=permission_callback,
|
|
620
|
+
)
|
|
621
|
+
except Exception as exc:
|
|
622
|
+
coder_result = f"(Coder agent failed: {exc})"
|
|
623
|
+
coder_meta = {}
|
|
624
|
+
|
|
625
|
+
steps.extend(coder_steps)
|
|
626
|
+
ctx.agent_findings[CODER] = coder_result
|
|
627
|
+
# Shared structured state (requirement #22): every later role
|
|
628
|
+
# sees the actual tool results instead of rediscovering them.
|
|
629
|
+
ctx.tool_results = [{"tool": t, "args": a, "result": str(r)[:400]}
|
|
630
|
+
for t, a, r, _c in coder_steps]
|
|
631
|
+
ctx.relevant_files = [
|
|
632
|
+
(a or {}).get("path") for t, a, _r, _c in coder_steps
|
|
633
|
+
if isinstance(a, dict) and a.get("path")]
|
|
634
|
+
ctx.decisions.append(f"Coder completed {len(coder_steps)} tool step(s)")
|
|
635
|
+
|
|
636
|
+
# Acquire file locks for any files the coder wrote
|
|
637
|
+
self._acquire_file_locks(coder_steps, "coder")
|
|
638
|
+
_report(CODER, "done", f"{len(coder_steps)} tool step(s)")
|
|
639
|
+
|
|
640
|
+
# ---- 5-8. INDEPENDENT REVIEW ROLES — run in PARALLEL ----------
|
|
641
|
+
# REVIEWER / SECURITY / DEBUGGER / TESTER / ARCHITECT /
|
|
642
|
+
# UI_DESIGNER all consume the SAME finished coder output and do
|
|
643
|
+
# not write files, so they are genuinely independent work
|
|
644
|
+
# (requirement #11/#24): executed concurrently on a thread pool,
|
|
645
|
+
# cutting wall-clock review time to roughly the slowest single
|
|
646
|
+
# role instead of the sum of all of them.
|
|
647
|
+
parallel_roles = [r for r in (SECURITY, DEBUGGER, REVIEWER, TESTER,
|
|
648
|
+
ARCHITECT, UI_DESIGNER) if r in roster]
|
|
649
|
+
if parallel_roles:
|
|
650
|
+
try:
|
|
651
|
+
from .event_stream import stream as _es
|
|
652
|
+
_es.emit(MULTI_AGENT_PROGRESS, source="multi_ai",
|
|
653
|
+
agent="review-team",
|
|
654
|
+
status="start",
|
|
655
|
+
summary=f"{len(parallel_roles)} reviewer(s) running concurrently")
|
|
656
|
+
except Exception:
|
|
657
|
+
pass
|
|
658
|
+
max_workers = min(4, len(parallel_roles))
|
|
659
|
+
with ThreadPoolExecutor(max_workers=max_workers,
|
|
660
|
+
thread_name_prefix="cat-review") as pool:
|
|
661
|
+
futures = {
|
|
662
|
+
r: pool.submit(
|
|
663
|
+
self._run_single_role, r, ctx, user_text, plan,
|
|
664
|
+
coder_result, steps, permission_callback, on_agent)
|
|
665
|
+
for r in parallel_roles
|
|
666
|
+
}
|
|
667
|
+
outputs = {}
|
|
668
|
+
for role_key, fut in futures.items():
|
|
669
|
+
try:
|
|
670
|
+
outputs[role_key] = fut.result()
|
|
671
|
+
except Exception as exc:
|
|
672
|
+
outputs[role_key] = ""
|
|
673
|
+
ctx.issues.append(f"{role_key}: reviewer crashed ({exc})")
|
|
674
|
+
for role_key, out in outputs.items():
|
|
675
|
+
data = _extract_json(out or "")
|
|
676
|
+
if isinstance(data, dict) and data.get("ok") is False and data.get("note"):
|
|
677
|
+
label = {"security": "Security", "debugger": "Debugger",
|
|
678
|
+
"reviewer": "Reviewer", "tester": "Tester",
|
|
679
|
+
"architect": "Architecture",
|
|
680
|
+
"ui_designer": "UI"}.get(role_key, role_key.title())
|
|
681
|
+
ctx.issues.append(f"{label}: {data['note']}")
|
|
682
|
+
ctx.agent_findings.setdefault(role_key, out or "")
|
|
683
|
+
|
|
684
|
+
# ---- 9. FINALIZER — merge everything into the final answer ---
|
|
685
|
+
final_text = coder_result
|
|
686
|
+
if FINALIZER in roster:
|
|
687
|
+
final_text = self._run_single_role(
|
|
688
|
+
FINALIZER, ctx, user_text, plan, coder_result, steps,
|
|
689
|
+
permission_callback, on_agent)
|
|
690
|
+
# Finalizer may have returned a polished version; fall back
|
|
691
|
+
# to coder_result if it returned empty/garbled output.
|
|
692
|
+
if not final_text or len(final_text.strip()) < 10:
|
|
693
|
+
final_text = coder_result
|
|
694
|
+
|
|
695
|
+
# Append a standard issues/flags block if any reviewer flagged issues
|
|
696
|
+
if ctx.issues:
|
|
697
|
+
flag_lines = "\n".join(f" {i}" for i in ctx.issues)
|
|
698
|
+
final_text = final_text.rstrip() + \
|
|
699
|
+
f"\n\n[Agent Review Flags]\n{flag_lines}\n"
|
|
700
|
+
|
|
701
|
+
# ---- 10. Release file locks ----------------------------------
|
|
702
|
+
self._release_file_locks("coder")
|
|
703
|
+
|
|
704
|
+
# ---- 11. Build metadata --------------------------------------
|
|
705
|
+
meta: Dict[str, Any] = {
|
|
706
|
+
"roster": [a["key"] for a in agents],
|
|
707
|
+
"plan": plan,
|
|
708
|
+
"research_brief": research_brief,
|
|
709
|
+
"issues": ctx.issues,
|
|
710
|
+
"decisions": ctx.decisions,
|
|
711
|
+
"tool_steps": len(steps),
|
|
712
|
+
}
|
|
713
|
+
_report("orchestrator", "done",
|
|
714
|
+
f"{len(agents)} agent events, {len(ctx.issues)} issue(s)")
|
|
715
|
+
try:
|
|
716
|
+
from .event_stream import stream as _es, MULTI_AGENT_FINISHED
|
|
717
|
+
_es.emit(MULTI_AGENT_FINISHED, source="multi_ai",
|
|
718
|
+
agents=len(agents), issues=len(ctx.issues),
|
|
719
|
+
tool_steps=len(steps))
|
|
720
|
+
except Exception:
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
return final_text, steps, meta, agents
|