caudate-cli 0.1.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.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
core/worktree.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Worktree isolation — give a subagent its own checkout to mutate.
|
|
2
|
+
|
|
3
|
+
A worktree is a separate working copy backed by the same `.git/` repo,
|
|
4
|
+
created with `git worktree add`. The subagent gets full read/write
|
|
5
|
+
access to its own copy without touching the parent's working tree. When
|
|
6
|
+
the subagent finishes, the worktree is either kept (if it produced
|
|
7
|
+
changes) or cleaned up (if it didn't), mirroring the SDK's contract.
|
|
8
|
+
|
|
9
|
+
Two failure modes are handled gracefully:
|
|
10
|
+
- The repo is dirty in a way that blocks `worktree add` → return
|
|
11
|
+
`None`, caller should fall back to the shared tree.
|
|
12
|
+
- `git` isn't installed or the cwd isn't a git repo → same.
|
|
13
|
+
|
|
14
|
+
Callers should treat worktree creation as best-effort: a failure is
|
|
15
|
+
not an error, just a missed optimization.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import shutil
|
|
23
|
+
import tempfile
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Worktree:
|
|
33
|
+
path: Path
|
|
34
|
+
branch: str
|
|
35
|
+
parent_repo: Path
|
|
36
|
+
|
|
37
|
+
async def has_changes(self) -> bool:
|
|
38
|
+
"""Did the subagent write anything?"""
|
|
39
|
+
rc, out, _ = await _git(self.path, "status", "--porcelain")
|
|
40
|
+
return rc == 0 and bool(out.strip())
|
|
41
|
+
|
|
42
|
+
async def cleanup(self, force: bool = False) -> None:
|
|
43
|
+
"""Remove the worktree. Skips if it has uncommitted changes (unless force)."""
|
|
44
|
+
if not force and await self.has_changes():
|
|
45
|
+
logger.info(
|
|
46
|
+
f"Keeping worktree {self.path} — has uncommitted changes"
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
rc, _, err = await _git(
|
|
50
|
+
self.parent_repo, "worktree", "remove", "--force", str(self.path),
|
|
51
|
+
)
|
|
52
|
+
if rc != 0:
|
|
53
|
+
logger.debug(f"worktree remove failed: {err}")
|
|
54
|
+
shutil.rmtree(self.path, ignore_errors=True)
|
|
55
|
+
# Best-effort branch delete
|
|
56
|
+
await _git(self.parent_repo, "branch", "-D", self.branch)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def create_worktree(
|
|
60
|
+
parent_repo: Path | None = None,
|
|
61
|
+
base_branch: str = "HEAD",
|
|
62
|
+
) -> Worktree | None:
|
|
63
|
+
"""Create a fresh worktree off `base_branch`. Returns None on any failure."""
|
|
64
|
+
repo = parent_repo or Path.cwd()
|
|
65
|
+
if shutil.which("git") is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
rc, out, _ = await _git(repo, "rev-parse", "--show-toplevel")
|
|
69
|
+
if rc != 0:
|
|
70
|
+
return None
|
|
71
|
+
repo = Path(out.strip())
|
|
72
|
+
|
|
73
|
+
branch = f"cognos/agent-{uuid.uuid4().hex[:8]}"
|
|
74
|
+
tmp = Path(tempfile.mkdtemp(prefix="cognos-wt-"))
|
|
75
|
+
rc, _, err = await _git(
|
|
76
|
+
repo, "worktree", "add", "-b", branch, str(tmp), base_branch,
|
|
77
|
+
)
|
|
78
|
+
if rc != 0:
|
|
79
|
+
logger.debug(f"worktree add failed: {err}")
|
|
80
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
81
|
+
return None
|
|
82
|
+
return Worktree(path=tmp, branch=branch, parent_repo=repo)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _git(cwd: Path, *args: str) -> tuple[int, str, str]:
|
|
86
|
+
proc = await asyncio.create_subprocess_exec(
|
|
87
|
+
"git", *args,
|
|
88
|
+
cwd=str(cwd),
|
|
89
|
+
stdout=asyncio.subprocess.PIPE,
|
|
90
|
+
stderr=asyncio.subprocess.PIPE,
|
|
91
|
+
)
|
|
92
|
+
out, err = await proc.communicate()
|
|
93
|
+
return proc.returncode or 0, out.decode(errors="ignore"), err.decode(errors="ignore")
|
execution/__init__.py
ADDED
|
File without changes
|
execution/executor.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Executor — manages tool registry and executes tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from core.schemas import Task, ToolResult, ToolResultStatus, Episode
|
|
11
|
+
from execution.tools.base import BaseTool
|
|
12
|
+
from execution.tools.shell_tool import PowerShellTool, ShellTool
|
|
13
|
+
from execution.tools.file_tool import FileReadTool, FileWriteTool
|
|
14
|
+
from execution.tools.edit_tool import EditTool, FillInMiddleTool
|
|
15
|
+
from execution.tools.glob_tool import GlobTool
|
|
16
|
+
from execution.tools.grep_tool import GrepTool
|
|
17
|
+
from execution.tools.notebook_tool import NotebookEditTool
|
|
18
|
+
from execution.tools.ask_user_question_tool import AskUserQuestionTool
|
|
19
|
+
from execution.tools.push_notification_tool import PushNotificationTool
|
|
20
|
+
from execution.tools.cron_tool import CronCreateTool, CronDeleteTool, CronListTool
|
|
21
|
+
from execution.tools.task_tool import (
|
|
22
|
+
TaskCreateTool, TaskGetTool, TaskListTool, TaskOutputTool, TaskStopTool,
|
|
23
|
+
)
|
|
24
|
+
from execution.tools.worktree_tool import EnterWorktreeTool, ExitWorktreeTool
|
|
25
|
+
from execution.tools.mcp_tool import MCPListServersTool
|
|
26
|
+
from execution.tools.plan_mode_tool import EnterPlanModeTool, ExitPlanModeTool
|
|
27
|
+
from execution.tools.find_anywhere_tool import FindAnywhereTool
|
|
28
|
+
from execution.tools.system_info_tool import SystemInfoTool
|
|
29
|
+
from execution.tools.update_memory_tool import UpdateMemoryTool
|
|
30
|
+
from execution.tools.artifact_tool import ArtifactTool
|
|
31
|
+
from execution.tools.calculator_tool import CalculatorTool
|
|
32
|
+
from execution.tools.datetime_tool import DateTimeTool
|
|
33
|
+
from execution.tools.semantic_search_tool import SemanticSearchTool
|
|
34
|
+
from execution.tools.describe_image_tool import DescribeImageTool
|
|
35
|
+
from execution.tools.transcribe_audio_tool import TranscribeAudioTool
|
|
36
|
+
from execution.tools.speak_tool import SpeakTool
|
|
37
|
+
from execution.tools.http_request_tool import HttpRequestTool
|
|
38
|
+
from execution.tools.sandbox_tool import SandboxTool
|
|
39
|
+
from execution.tools.agentic_tool import AgenticTool, AgenticBrowseTool
|
|
40
|
+
from execution.tools.openapi_tool import OpenAPITool
|
|
41
|
+
from execution.tools.load_skill_tool import LoadSkillTool, RescanSkillsTool
|
|
42
|
+
from execution.tools.cognos_card_tool import CognosCardTool
|
|
43
|
+
from execution.tools.search_tool import WebSearchTool
|
|
44
|
+
from execution.tools.web_fetch_tool import WebFetchTool
|
|
45
|
+
from execution.tools.python_tool import PythonExecTool
|
|
46
|
+
from execution.tools.respond_tool import RespondTool
|
|
47
|
+
from execution.tools.think_tool import ThinkTool
|
|
48
|
+
from execution.tools.agent_tool import AgentTool
|
|
49
|
+
from execution.tools.draw_tool import DrawTool
|
|
50
|
+
from execution.tools.edit_image_tool import EditImageTool
|
|
51
|
+
from execution.tools.longcat_avatar_tool import LongCatAvatarTool
|
|
52
|
+
from execution.tools.storyboard_tool import StoryboardTool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Legacy tool names mapped to their current canonical names. Kept for
|
|
56
|
+
# backward-compat with plans/strategies stored from Phase 1.
|
|
57
|
+
_LEGACY_ALIASES = {
|
|
58
|
+
"shell": "Bash",
|
|
59
|
+
"file_read": "Read",
|
|
60
|
+
"file_write": "Write",
|
|
61
|
+
"web_search": "WebSearch",
|
|
62
|
+
"python_exec": "PythonExec",
|
|
63
|
+
"respond": "Respond",
|
|
64
|
+
"think": "Think",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Executor:
|
|
71
|
+
"""Tool registry and task execution engine."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, llm=None, load_plugins: bool = True):
|
|
74
|
+
self._tools: dict[str, BaseTool] = {}
|
|
75
|
+
self._llm = llm
|
|
76
|
+
self._register_defaults()
|
|
77
|
+
if load_plugins:
|
|
78
|
+
self._load_plugins()
|
|
79
|
+
|
|
80
|
+
def _load_plugins(self) -> None:
|
|
81
|
+
"""Auto-load community plugins from PLUGINS_DIR."""
|
|
82
|
+
try:
|
|
83
|
+
from config import PLUGINS_DIR
|
|
84
|
+
from execution.plugins import load_plugins as _load
|
|
85
|
+
count = _load(PLUGINS_DIR, self)
|
|
86
|
+
if count:
|
|
87
|
+
logger.info(f"Loaded {count} plugin tool(s) from {PLUGINS_DIR}")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Plugin loader failed: {e}")
|
|
90
|
+
|
|
91
|
+
def _register_defaults(self) -> None:
|
|
92
|
+
"""Register built-in tools."""
|
|
93
|
+
think_tool = ThinkTool(llm=self._llm)
|
|
94
|
+
agent_tool = AgentTool() # manager wired later by CognosAgent
|
|
95
|
+
# Read image-gen config from settings so users can swap model
|
|
96
|
+
# (e.g. FLUX.1-schnell vs SDXL-Turbo) without code changes.
|
|
97
|
+
# Falls back to DrawTool's own defaults if no `image` block.
|
|
98
|
+
try:
|
|
99
|
+
from core.settings import Settings
|
|
100
|
+
_img_cfg = Settings.load().get("image", {}) or {}
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.warning(f"Failed to load image settings: {e}")
|
|
103
|
+
_img_cfg = {}
|
|
104
|
+
_img_backend = _img_cfg.get("backend", "diffusers")
|
|
105
|
+
_img_kwargs = {k: v for k, v in _img_cfg.items() if k != "backend"}
|
|
106
|
+
draw_tool = DrawTool(
|
|
107
|
+
backend=_img_backend, backend_kwargs=_img_kwargs,
|
|
108
|
+
) # file_store wired later by CognosAgent
|
|
109
|
+
# Image-edit (img2img + Kontext). Reads `image_edit` settings
|
|
110
|
+
# block; falls back to defaults baked into DiffusersImageEdit.
|
|
111
|
+
try:
|
|
112
|
+
from core.settings import Settings
|
|
113
|
+
_edit_cfg = Settings.load().get("image_edit", {}) or {}
|
|
114
|
+
except Exception:
|
|
115
|
+
_edit_cfg = {}
|
|
116
|
+
_edit_backend = _edit_cfg.get("backend", "diffusers")
|
|
117
|
+
_edit_kwargs = {k: v for k, v in _edit_cfg.items() if k != "backend"}
|
|
118
|
+
edit_image_tool = EditImageTool(
|
|
119
|
+
backend=_edit_backend, backend_kwargs=_edit_kwargs,
|
|
120
|
+
) # file_store wired later by CognosAgent
|
|
121
|
+
storyboard_tool = StoryboardTool(
|
|
122
|
+
llm=self._llm,
|
|
123
|
+
) # file_store + image backends wired later by CognosAgent
|
|
124
|
+
# LongCat Avatar (WAN-S2V talking-head video) — drives a local
|
|
125
|
+
# ComfyUI server. URL overridable via COGNOS_COMFYUI_URL.
|
|
126
|
+
longcat_avatar_tool = LongCatAvatarTool(
|
|
127
|
+
comfyui_url=os.environ.get(
|
|
128
|
+
"COGNOS_COMFYUI_URL", "http://127.0.0.1:8188",
|
|
129
|
+
),
|
|
130
|
+
) # file_store wired later by CognosAgent
|
|
131
|
+
artifact_tool = ArtifactTool() # file_store wired later by CognosAgent
|
|
132
|
+
speak_tool = SpeakTool() # file_store wired later by CognosAgent
|
|
133
|
+
for tool in [
|
|
134
|
+
ShellTool(),
|
|
135
|
+
PowerShellTool(),
|
|
136
|
+
FileReadTool(),
|
|
137
|
+
FileWriteTool(),
|
|
138
|
+
EditTool(),
|
|
139
|
+
FillInMiddleTool(),
|
|
140
|
+
GlobTool(),
|
|
141
|
+
GrepTool(),
|
|
142
|
+
NotebookEditTool(),
|
|
143
|
+
AskUserQuestionTool(),
|
|
144
|
+
PushNotificationTool(),
|
|
145
|
+
# Cron registry (read/create/delete)
|
|
146
|
+
CronListTool(),
|
|
147
|
+
CronCreateTool(),
|
|
148
|
+
CronDeleteTool(),
|
|
149
|
+
# Background task pool (TaskCreate needs the live agent —
|
|
150
|
+
# wired by CognosAgent.__init__ via set_agent())
|
|
151
|
+
TaskCreateTool(),
|
|
152
|
+
TaskListTool(),
|
|
153
|
+
TaskGetTool(),
|
|
154
|
+
TaskOutputTool(),
|
|
155
|
+
TaskStopTool(),
|
|
156
|
+
# MCP server visibility (needs the live agent)
|
|
157
|
+
MCPListServersTool(),
|
|
158
|
+
# Git worktrees
|
|
159
|
+
EnterWorktreeTool(),
|
|
160
|
+
ExitWorktreeTool(),
|
|
161
|
+
# Plan mode
|
|
162
|
+
EnterPlanModeTool(),
|
|
163
|
+
ExitPlanModeTool(),
|
|
164
|
+
FindAnywhereTool(),
|
|
165
|
+
SystemInfoTool(),
|
|
166
|
+
UpdateMemoryTool(),
|
|
167
|
+
CalculatorTool(),
|
|
168
|
+
DateTimeTool(),
|
|
169
|
+
SemanticSearchTool(),
|
|
170
|
+
DescribeImageTool(),
|
|
171
|
+
TranscribeAudioTool(),
|
|
172
|
+
HttpRequestTool(),
|
|
173
|
+
SandboxTool(),
|
|
174
|
+
AgenticTool(),
|
|
175
|
+
AgenticBrowseTool(),
|
|
176
|
+
OpenAPITool(),
|
|
177
|
+
LoadSkillTool(),
|
|
178
|
+
RescanSkillsTool(),
|
|
179
|
+
CognosCardTool(),
|
|
180
|
+
WebSearchTool(),
|
|
181
|
+
WebFetchTool(),
|
|
182
|
+
PythonExecTool(),
|
|
183
|
+
RespondTool(),
|
|
184
|
+
think_tool,
|
|
185
|
+
agent_tool,
|
|
186
|
+
draw_tool,
|
|
187
|
+
edit_image_tool,
|
|
188
|
+
longcat_avatar_tool,
|
|
189
|
+
storyboard_tool,
|
|
190
|
+
artifact_tool,
|
|
191
|
+
speak_tool,
|
|
192
|
+
]:
|
|
193
|
+
self.register_tool(tool)
|
|
194
|
+
|
|
195
|
+
# Forge feature-CRUD tools — let the agent manage its own
|
|
196
|
+
# backlog when the orchestrator binds a project context.
|
|
197
|
+
# Outside of a forge run they refuse politely.
|
|
198
|
+
try:
|
|
199
|
+
from execution.tools.forge_feature_tools import FORGE_FEATURE_TOOLS
|
|
200
|
+
for cls in FORGE_FEATURE_TOOLS:
|
|
201
|
+
self.register_tool(cls())
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.warning(f"Forge tool registration failed: {e}")
|
|
204
|
+
|
|
205
|
+
def set_llm(self, llm) -> None:
|
|
206
|
+
"""Set or update the LLM for tools that need it."""
|
|
207
|
+
self._llm = llm
|
|
208
|
+
think = self.get_tool("think")
|
|
209
|
+
if think and hasattr(think, "set_llm"):
|
|
210
|
+
think.set_llm(llm)
|
|
211
|
+
|
|
212
|
+
def register_tool(self, tool: BaseTool) -> None:
|
|
213
|
+
"""Register a tool."""
|
|
214
|
+
self._tools[tool.name] = tool
|
|
215
|
+
logger.debug(f"Registered tool: {tool.name}")
|
|
216
|
+
|
|
217
|
+
def get_tool(self, name: str) -> BaseTool | None:
|
|
218
|
+
if name in self._tools:
|
|
219
|
+
return self._tools[name]
|
|
220
|
+
# Fall back to legacy aliases
|
|
221
|
+
alias = _LEGACY_ALIASES.get(name)
|
|
222
|
+
if alias and alias in self._tools:
|
|
223
|
+
return self._tools[alias]
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def list_tools(self) -> list[str]:
|
|
227
|
+
return list(self._tools.keys())
|
|
228
|
+
|
|
229
|
+
def tool_definitions(self) -> list[dict]:
|
|
230
|
+
"""Get LiteLLM-compatible tool definitions for tool calling."""
|
|
231
|
+
return [tool.to_tool_definition() for tool in self._tools.values()]
|
|
232
|
+
|
|
233
|
+
def tool_descriptions(self) -> str:
|
|
234
|
+
"""Get descriptions for LLM context (legacy/display use)."""
|
|
235
|
+
lines = []
|
|
236
|
+
for name, tool in self._tools.items():
|
|
237
|
+
params = ", ".join(f"{k}: {v}" for k, v in tool.parameters.items())
|
|
238
|
+
lines.append(f"- {name}: {tool.description} ({params})")
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
async def execute_tool(self, name: str, args: dict) -> ToolResult:
|
|
242
|
+
"""Execute a tool by name with raw args (for agentic loop).
|
|
243
|
+
|
|
244
|
+
Unlike execute_task(), this takes raw name+args without a Task object.
|
|
245
|
+
"""
|
|
246
|
+
tool = self.get_tool(name)
|
|
247
|
+
if tool is None:
|
|
248
|
+
return ToolResult(
|
|
249
|
+
tool_name=name,
|
|
250
|
+
status=ToolResultStatus.ERROR,
|
|
251
|
+
error=f"Unknown tool: {name}",
|
|
252
|
+
)
|
|
253
|
+
# Plan-mode gate: refuse any tool that mutates state. The two
|
|
254
|
+
# exceptions are EnterPlanMode/ExitPlanMode themselves so the
|
|
255
|
+
# agent can always toggle out.
|
|
256
|
+
try:
|
|
257
|
+
from core.plan_mode import is_active as _plan_active
|
|
258
|
+
if (
|
|
259
|
+
_plan_active()
|
|
260
|
+
and getattr(tool, "mutates", False)
|
|
261
|
+
and name not in ("EnterPlanMode", "ExitPlanMode")
|
|
262
|
+
):
|
|
263
|
+
return ToolResult(
|
|
264
|
+
tool_name=name,
|
|
265
|
+
status=ToolResultStatus.ERROR,
|
|
266
|
+
error=(
|
|
267
|
+
f"Plan mode is ON — `{name}` mutates state "
|
|
268
|
+
"and is blocked. Read-only tools still work. "
|
|
269
|
+
"Call ExitPlanMode to allow mutations."
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
except Exception:
|
|
273
|
+
pass # plan_mode module missing → behave as if mode is off
|
|
274
|
+
try:
|
|
275
|
+
return await tool.execute(**args)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return ToolResult(
|
|
278
|
+
tool_name=name,
|
|
279
|
+
status=ToolResultStatus.ERROR,
|
|
280
|
+
error=str(e),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def execute_task(self, task: Task) -> tuple[ToolResult, Episode]:
|
|
284
|
+
"""Execute a single task and return result + episode record."""
|
|
285
|
+
task.attempts += 1
|
|
286
|
+
start = time.time()
|
|
287
|
+
|
|
288
|
+
# Normalize "null"/"none" strings to actual None
|
|
289
|
+
if task.tool_name and task.tool_name.lower() in ("null", "none", "reasoning"):
|
|
290
|
+
task.tool_name = None
|
|
291
|
+
|
|
292
|
+
if not task.tool_name:
|
|
293
|
+
# Pure reasoning task — no tool to execute
|
|
294
|
+
result = ToolResult(
|
|
295
|
+
tool_name="reasoning",
|
|
296
|
+
status=ToolResultStatus.SUCCESS,
|
|
297
|
+
output=task.description,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
tool = self.get_tool(task.tool_name)
|
|
301
|
+
if tool is None:
|
|
302
|
+
result = ToolResult(
|
|
303
|
+
tool_name=task.tool_name,
|
|
304
|
+
status=ToolResultStatus.ERROR,
|
|
305
|
+
error=f"Unknown tool: {task.tool_name}",
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
try:
|
|
309
|
+
result = await tool.execute(**task.tool_args)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
result = ToolResult(
|
|
312
|
+
tool_name=task.tool_name,
|
|
313
|
+
status=ToolResultStatus.ERROR,
|
|
314
|
+
error=str(e),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
result.duration_ms = (time.time() - start) * 1000
|
|
318
|
+
|
|
319
|
+
episode = Episode(
|
|
320
|
+
goal_id=task.goal_id,
|
|
321
|
+
task_id=task.id,
|
|
322
|
+
action=task.description,
|
|
323
|
+
tool_name=task.tool_name,
|
|
324
|
+
tool_args=task.tool_args,
|
|
325
|
+
result=result,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
logger.info(f"Executed task '{task.description}': {result.status.value} ({result.duration_ms:.0f}ms)")
|
|
329
|
+
return result, episode
|
execution/plugins.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Plugin loader — discover and register community tools from a directory.
|
|
2
|
+
|
|
3
|
+
Each plugin is a Python file in the plugins directory that exposes ONE of:
|
|
4
|
+
|
|
5
|
+
1. A module-level variable `PLUGIN` assigned to a BaseTool subclass instance:
|
|
6
|
+
from execution.tools.base import BaseTool
|
|
7
|
+
class Hello(BaseTool):
|
|
8
|
+
...
|
|
9
|
+
PLUGIN = Hello()
|
|
10
|
+
|
|
11
|
+
2. A module-level `register(executor)` callable that registers tools itself:
|
|
12
|
+
def register(executor):
|
|
13
|
+
executor.register_tool(MyToolA())
|
|
14
|
+
executor.register_tool(MyToolB())
|
|
15
|
+
|
|
16
|
+
Plugins are discovered from `plugins/*.py` (skipping files whose names start
|
|
17
|
+
with `_`) and `plugins/*/plugin.py` for package-style plugins.
|
|
18
|
+
|
|
19
|
+
Failures in one plugin never prevent loading the others — they're logged
|
|
20
|
+
and skipped.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import importlib.util
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from execution.tools.base import BaseTool
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_plugins(plugin_dir: Path, executor: Any) -> int:
|
|
37
|
+
"""Discover and register every plugin in `plugin_dir`. Returns count registered."""
|
|
38
|
+
if not plugin_dir.exists():
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
count = 0
|
|
42
|
+
for path in _discover(plugin_dir):
|
|
43
|
+
try:
|
|
44
|
+
registered = _load_one(path, executor)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"Plugin {path} failed to load: {e}")
|
|
47
|
+
continue
|
|
48
|
+
count += registered
|
|
49
|
+
return count
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _discover(plugin_dir: Path) -> list[Path]:
|
|
53
|
+
"""Return every plugin entry point under `plugin_dir`.
|
|
54
|
+
|
|
55
|
+
- Top-level files: plugin_dir/*.py (skipping _-prefixed and __init__)
|
|
56
|
+
- Package style: plugin_dir/<name>/plugin.py
|
|
57
|
+
"""
|
|
58
|
+
entries: list[Path] = []
|
|
59
|
+
for path in sorted(plugin_dir.glob("*.py")):
|
|
60
|
+
name = path.stem
|
|
61
|
+
if name.startswith("_"):
|
|
62
|
+
continue
|
|
63
|
+
entries.append(path)
|
|
64
|
+
for path in sorted(plugin_dir.glob("*/plugin.py")):
|
|
65
|
+
entries.append(path)
|
|
66
|
+
return entries
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_one(path: Path, executor: Any) -> int:
|
|
70
|
+
"""Import `path` as a module and register whatever it exposes."""
|
|
71
|
+
module_name = f"cognos_plugin_{path.parent.name}_{path.stem}"
|
|
72
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
73
|
+
if spec is None or spec.loader is None:
|
|
74
|
+
raise ImportError(f"Cannot load spec for {path}")
|
|
75
|
+
module = importlib.util.module_from_spec(spec)
|
|
76
|
+
sys.modules[module_name] = module
|
|
77
|
+
spec.loader.exec_module(module)
|
|
78
|
+
|
|
79
|
+
registered = 0
|
|
80
|
+
|
|
81
|
+
# Preferred: explicit register() callable
|
|
82
|
+
register_fn = getattr(module, "register", None)
|
|
83
|
+
if callable(register_fn):
|
|
84
|
+
before = len(executor.list_tools())
|
|
85
|
+
register_fn(executor)
|
|
86
|
+
registered = len(executor.list_tools()) - before
|
|
87
|
+
logger.info(f"Plugin {path.name}: register() added {registered} tool(s)")
|
|
88
|
+
return registered
|
|
89
|
+
|
|
90
|
+
# Alternative: a PLUGIN variable pointing to a BaseTool instance (or list)
|
|
91
|
+
plugin_obj = getattr(module, "PLUGIN", None)
|
|
92
|
+
if plugin_obj is None:
|
|
93
|
+
logger.warning(
|
|
94
|
+
f"Plugin {path.name} has neither PLUGIN nor register() — skipping"
|
|
95
|
+
)
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
candidates = plugin_obj if isinstance(plugin_obj, (list, tuple)) else [plugin_obj]
|
|
99
|
+
for obj in candidates:
|
|
100
|
+
if not isinstance(obj, BaseTool):
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Plugin {path.name} PLUGIN entry {obj!r} is not a BaseTool instance"
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
executor.register_tool(obj)
|
|
106
|
+
registered += 1
|
|
107
|
+
logger.info(f"Plugin {path.name}: registered {registered} tool(s)")
|
|
108
|
+
return registered
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Agent tool — the Respond-like primitive that lets the parent spawn subagents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from core.schemas import ToolResult
|
|
8
|
+
from execution.tools.base import BaseTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentTool(BaseTool):
|
|
12
|
+
name = "Agent"
|
|
13
|
+
description = (
|
|
14
|
+
"Spawn a subagent OR dispatch a message on the inter-agent bus.\n\n"
|
|
15
|
+
"Spawn mode (default): supply `task` (+ optional `subagent_type`, "
|
|
16
|
+
"`isolation`) to spawn a child agent and wait for its summary. "
|
|
17
|
+
"Available subagent_types: general-purpose, research, code-review, "
|
|
18
|
+
"executor_brain, critic_brain.\n\n"
|
|
19
|
+
"Bus mode: supply `recipient_agent_id` "
|
|
20
|
+
"(executor_brain_01 or critic_brain_02) plus `payload_context`. "
|
|
21
|
+
"Set `suspend_sender_state=true` to wait for the recipient's "
|
|
22
|
+
"reply; otherwise fire-and-forget."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def input_schema(self) -> dict:
|
|
27
|
+
return {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"task": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Spawn mode: what the subagent should accomplish. Be specific — the subagent does not see your context.",
|
|
33
|
+
},
|
|
34
|
+
"subagent_type": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Spawn mode preset",
|
|
37
|
+
"enum": ["general-purpose", "research", "code-review",
|
|
38
|
+
"executor_brain", "critic_brain"],
|
|
39
|
+
"default": "general-purpose",
|
|
40
|
+
},
|
|
41
|
+
"isolation": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Spawn mode: set 'worktree' for an isolated working copy. Best-effort.",
|
|
44
|
+
"enum": ["worktree"],
|
|
45
|
+
},
|
|
46
|
+
"recipient_agent_id": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Bus mode: which agent to message.",
|
|
49
|
+
"enum": ["executor_brain_01", "critic_brain_02"],
|
|
50
|
+
},
|
|
51
|
+
"payload_context": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Bus mode: message body delivered to the recipient.",
|
|
54
|
+
},
|
|
55
|
+
"suspend_sender_state": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"description": "Bus mode: if true, await the recipient's reply before returning.",
|
|
58
|
+
"default": False,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def __init__(self, manager=None):
|
|
64
|
+
self._manager = manager
|
|
65
|
+
|
|
66
|
+
def set_manager(self, manager) -> None:
|
|
67
|
+
self._manager = manager
|
|
68
|
+
|
|
69
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
70
|
+
if self._manager is None:
|
|
71
|
+
return self._error("SubagentManager not configured")
|
|
72
|
+
|
|
73
|
+
recipient = kwargs.get("recipient_agent_id")
|
|
74
|
+
if recipient:
|
|
75
|
+
payload = kwargs.get("payload_context", "")
|
|
76
|
+
if not payload:
|
|
77
|
+
return self._error("payload_context required in bus mode")
|
|
78
|
+
suspend = bool(kwargs.get("suspend_sender_state", False))
|
|
79
|
+
try:
|
|
80
|
+
reply = await self._manager.dispatch(
|
|
81
|
+
sender_id="agent_tool",
|
|
82
|
+
recipient_id=recipient,
|
|
83
|
+
context=payload,
|
|
84
|
+
suspend_sender=suspend,
|
|
85
|
+
)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return self._error(f"Bus dispatch failed: {e}")
|
|
88
|
+
if suspend:
|
|
89
|
+
return self._success(str(reply) if reply is not None else "(no reply)")
|
|
90
|
+
return self._success(f"Message queued for {recipient}.")
|
|
91
|
+
|
|
92
|
+
task = kwargs.get("task", "")
|
|
93
|
+
subagent_type = kwargs.get("subagent_type", "general-purpose")
|
|
94
|
+
isolation = kwargs.get("isolation")
|
|
95
|
+
|
|
96
|
+
if not task:
|
|
97
|
+
return self._error("No task provided (and no recipient_agent_id for bus mode)")
|
|
98
|
+
|
|
99
|
+
run = await self._manager.run_one(
|
|
100
|
+
task, subagent_type=subagent_type, isolation=isolation,
|
|
101
|
+
)
|
|
102
|
+
if run.error:
|
|
103
|
+
return self._error(f"Subagent {run.id} failed: {run.error}")
|
|
104
|
+
result = run.result or "(no output)"
|
|
105
|
+
if run.worktree_path:
|
|
106
|
+
result += f"\n\n[worktree: {run.worktree_path} branch={run.worktree_branch}]"
|
|
107
|
+
return self._success(result)
|