crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""SkillTool — lets the model invoke user-defined skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from crabcode_core.types.tool import PermissionBehavior, PermissionResult, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from crabcode_core.skills.loader import SkillDefinition
|
|
11
|
+
from crabcode_core.types.tool import ToolContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillTool(Tool):
|
|
15
|
+
"""Execute a user-defined skill loaded from SKILL.md files.
|
|
16
|
+
|
|
17
|
+
The model calls this tool when the user invokes a skill by name (e.g.
|
|
18
|
+
``/commit``) or when the task naturally matches a skill's description.
|
|
19
|
+
The tool expands the skill's Markdown content and returns it so the model
|
|
20
|
+
can follow the instructions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name = "Skill"
|
|
24
|
+
is_read_only = True
|
|
25
|
+
is_concurrency_safe = True
|
|
26
|
+
|
|
27
|
+
input_schema: dict[str, Any] = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"skill_name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Name of the skill to execute.",
|
|
33
|
+
},
|
|
34
|
+
"user_input": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "The user's additional input or context for the skill.",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"required": ["skill_name"],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __init__(self, skills: list[SkillDefinition]) -> None:
|
|
43
|
+
self._skills = skills
|
|
44
|
+
self._skill_map = {s.name: s for s in skills}
|
|
45
|
+
|
|
46
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
47
|
+
lines = [
|
|
48
|
+
"Execute a user-defined skill.\n",
|
|
49
|
+
"When a user invokes /<skill-name> or their request clearly matches a skill's",
|
|
50
|
+
"description, call this tool with the matching skill_name BEFORE generating",
|
|
51
|
+
"any other response. The tool will return the skill's instructions, which you",
|
|
52
|
+
"should then follow to complete the task.\n",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if self._skills:
|
|
56
|
+
lines.append("## Available skills\n")
|
|
57
|
+
for skill in self._skills:
|
|
58
|
+
summary = skill.when_to_use or skill.description
|
|
59
|
+
lines.append(f"- {skill.name}: {summary}")
|
|
60
|
+
|
|
61
|
+
lines.append(
|
|
62
|
+
"IMPORTANT: Only invoke skills listed above. "
|
|
63
|
+
"Do not guess or invent skill names."
|
|
64
|
+
)
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
|
|
68
|
+
skill_name = tool_input.get("skill_name", "")
|
|
69
|
+
if skill_name not in self._skill_map:
|
|
70
|
+
available = ", ".join(self._skill_map) or "(none)"
|
|
71
|
+
return f"Unknown skill '{skill_name}'. Available skills: {available}"
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def check_permissions(
|
|
75
|
+
self,
|
|
76
|
+
tool_input: dict[str, Any],
|
|
77
|
+
context: ToolContext,
|
|
78
|
+
) -> PermissionResult:
|
|
79
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW)
|
|
80
|
+
|
|
81
|
+
async def call(
|
|
82
|
+
self,
|
|
83
|
+
tool_input: dict[str, Any],
|
|
84
|
+
context: ToolContext,
|
|
85
|
+
) -> ToolResult:
|
|
86
|
+
skill_name = tool_input.get("skill_name", "")
|
|
87
|
+
user_input = tool_input.get("user_input", "")
|
|
88
|
+
|
|
89
|
+
skill = self._skill_map.get(skill_name)
|
|
90
|
+
if not skill:
|
|
91
|
+
available = ", ".join(self._skill_map) or "(none)"
|
|
92
|
+
return ToolResult(
|
|
93
|
+
is_error=True,
|
|
94
|
+
result_for_model=f"Unknown skill '{skill_name}'. Available: {available}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
content = skill.content
|
|
98
|
+
if user_input:
|
|
99
|
+
# Support explicit $USER_INPUT placeholder; otherwise append
|
|
100
|
+
if "$USER_INPUT" in content:
|
|
101
|
+
content = content.replace("$USER_INPUT", user_input)
|
|
102
|
+
else:
|
|
103
|
+
content = f"{content}\n\nUser input: {user_input}"
|
|
104
|
+
|
|
105
|
+
return ToolResult(
|
|
106
|
+
result_for_model=content,
|
|
107
|
+
result_for_display=f"[Skill: {skill_name}]",
|
|
108
|
+
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""SwitchMode tool — allows the agent to request a mode switch and submit plans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from crabcode_core.types.event import ModeChangeEvent, PlanReadyEvent
|
|
9
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SwitchModeTool(Tool):
|
|
13
|
+
name = "SwitchMode"
|
|
14
|
+
description = "Switch between plan mode (read-only planning) and agent mode (full execution). In plan mode, use this to submit a structured execution plan."
|
|
15
|
+
is_read_only = True
|
|
16
|
+
is_concurrency_safe = False
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"target_mode": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": ["agent", "plan"],
|
|
23
|
+
"description": "The mode to switch to.",
|
|
24
|
+
},
|
|
25
|
+
"explanation": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Brief explanation for why the mode switch is requested.",
|
|
28
|
+
},
|
|
29
|
+
"plan": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": (
|
|
32
|
+
"A structured execution plan to submit when switching from plan to agent mode. "
|
|
33
|
+
"Required when switching from plan to agent. Schema: "
|
|
34
|
+
'{"title": str, "summary": str, "steps": [{"id": str, "title": str, '
|
|
35
|
+
'"description": str, "files": [str], "depends_on": [str], "subagent_type": str}]}'
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"required": ["target_mode"],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Set by CoreSession before each query loop turn so the tool
|
|
43
|
+
# prompt can reflect the current mode.
|
|
44
|
+
current_mode: str = "agent"
|
|
45
|
+
|
|
46
|
+
def _build_description(self) -> str:
|
|
47
|
+
"""Build the dynamic tool description based on current_mode."""
|
|
48
|
+
base = self.description
|
|
49
|
+
if self.current_mode == "plan":
|
|
50
|
+
return (
|
|
51
|
+
base
|
|
52
|
+
+ " IMPORTANT: You are currently in plan mode — "
|
|
53
|
+
"only use target_mode='agent' (with a plan) to submit your plan. "
|
|
54
|
+
"Do NOT use target_mode='plan' as you are already in plan mode."
|
|
55
|
+
)
|
|
56
|
+
return (
|
|
57
|
+
base
|
|
58
|
+
+ " IMPORTANT: You are currently in agent mode — "
|
|
59
|
+
"only use target_mode='plan' if you want to switch to read-only planning. "
|
|
60
|
+
"Do NOT use target_mode='agent' as you are already in agent mode."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def to_api_schema(self) -> dict[str, Any]:
|
|
64
|
+
"""Override to use dynamic description instead of cached prompt."""
|
|
65
|
+
return {
|
|
66
|
+
"name": self.name,
|
|
67
|
+
"description": self._build_description(),
|
|
68
|
+
"input_schema": self.input_schema,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
72
|
+
mode_note = ""
|
|
73
|
+
if self.current_mode == "plan":
|
|
74
|
+
mode_note = (
|
|
75
|
+
"\n\nIMPORTANT: You are currently in plan mode. "
|
|
76
|
+
"Do NOT call this tool with target_mode='plan' — you are already in plan mode. "
|
|
77
|
+
"Use target_mode='agent' only when you are ready to submit your execution plan."
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
mode_note = (
|
|
81
|
+
"\n\nIMPORTANT: You are currently in agent mode. "
|
|
82
|
+
"Only call this tool if you want to switch to plan mode for read-only planning. "
|
|
83
|
+
"Do NOT call this tool with target_mode='agent' unless you are in plan mode "
|
|
84
|
+
"submitting a plan."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
"Switch between plan mode (read-only analysis and planning) and agent mode "
|
|
89
|
+
"(full tool access for executing changes).\n\n"
|
|
90
|
+
"Use cases:\n"
|
|
91
|
+
"- In plan mode: after gathering context and designing a plan, call this tool "
|
|
92
|
+
"with target_mode='agent' and a structured plan in the 'plan' field to submit "
|
|
93
|
+
"the plan to the interface for review. This ends the current turn. Do not call "
|
|
94
|
+
"other tools afterwards, and do not assume execution has started.\n"
|
|
95
|
+
"- In agent mode: call with target_mode='plan' to switch to read-only planning mode.\n\n"
|
|
96
|
+
"When submitting a plan (switching plan -> agent), the 'plan' field is required. "
|
|
97
|
+
"The plan should contain:\n"
|
|
98
|
+
"- title: short plan title\n"
|
|
99
|
+
"- summary: 1-3 sentence overview\n"
|
|
100
|
+
"- steps: array of execution steps, each with:\n"
|
|
101
|
+
" - id: unique short identifier (e.g. 's1', 's2')\n"
|
|
102
|
+
" - title: one-line step description\n"
|
|
103
|
+
" - description: detailed prompt for the sub-agent executing this step\n"
|
|
104
|
+
" - files: file paths this step will modify\n"
|
|
105
|
+
" - depends_on: ids of steps that must complete first (for DAG scheduling)\n"
|
|
106
|
+
" - subagent_type: 'generalPurpose' (default) or 'explore'\n"
|
|
107
|
+
" Do not set 'status' on steps — they are pending until execution.\n\n"
|
|
108
|
+
"Steps with no dependencies run in parallel. Design steps for maximum parallelism."
|
|
109
|
+
+ mode_note
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
|
|
113
|
+
target = tool_input.get("target_mode")
|
|
114
|
+
if target not in ("agent", "plan"):
|
|
115
|
+
return "target_mode must be 'agent' or 'plan'"
|
|
116
|
+
if target == self.current_mode:
|
|
117
|
+
return (
|
|
118
|
+
f"You are already in {target} mode. "
|
|
119
|
+
f"Switching to the same mode is unnecessary — continue with your current work."
|
|
120
|
+
)
|
|
121
|
+
if self.current_mode == "plan" and target == "agent":
|
|
122
|
+
plan_data = tool_input.get("plan")
|
|
123
|
+
if not isinstance(plan_data, dict) or not plan_data:
|
|
124
|
+
return (
|
|
125
|
+
"A non-empty structured plan is required when submitting "
|
|
126
|
+
"a plan-mode result for review"
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
async def call(
|
|
131
|
+
self,
|
|
132
|
+
tool_input: dict[str, Any],
|
|
133
|
+
context: ToolContext,
|
|
134
|
+
) -> ToolResult:
|
|
135
|
+
target_mode = tool_input["target_mode"]
|
|
136
|
+
explanation = tool_input.get("explanation", "")
|
|
137
|
+
plan_data = tool_input.get("plan")
|
|
138
|
+
|
|
139
|
+
# This is a security boundary as well as input validation. A model can
|
|
140
|
+
# call tools directly even when a provider does not enforce every JSON
|
|
141
|
+
# schema constraint, so never let a bare plan -> agent request emit a
|
|
142
|
+
# mode-change event.
|
|
143
|
+
if self.current_mode == "plan" and target_mode == "agent" and (
|
|
144
|
+
not isinstance(plan_data, dict) or not plan_data
|
|
145
|
+
):
|
|
146
|
+
return ToolResult(
|
|
147
|
+
result_for_model=(
|
|
148
|
+
"Cannot leave plan mode without submitting a non-empty "
|
|
149
|
+
"structured plan for user review."
|
|
150
|
+
),
|
|
151
|
+
is_error=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if target_mode == "agent" and plan_data:
|
|
155
|
+
from crabcode_core.plan.types import ExecutionPlan
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
plan = ExecutionPlan.from_dict(plan_data)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return ToolResult(
|
|
161
|
+
result_for_model=f"Invalid plan structure: {e}",
|
|
162
|
+
is_error=True,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
errors = plan.validate_dag()
|
|
166
|
+
if errors:
|
|
167
|
+
return ToolResult(
|
|
168
|
+
result_for_model=f"Plan validation failed:\n" + "\n".join(f"- {e}" for e in errors),
|
|
169
|
+
is_error=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
plan.prepare_for_execution()
|
|
173
|
+
if context.session and hasattr(context.session, "set_plan"):
|
|
174
|
+
context.session.set_plan(plan.to_dict())
|
|
175
|
+
|
|
176
|
+
if context.tool_event_queue:
|
|
177
|
+
await context.tool_event_queue.put(
|
|
178
|
+
PlanReadyEvent(plan=plan.to_dict())
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# A plan submission is not approval to execute it. Keep the session in
|
|
182
|
+
# plan mode until the interface receives an explicit user confirmation.
|
|
183
|
+
# Agent -> plan requests still take effect immediately.
|
|
184
|
+
submitting_plan = (
|
|
185
|
+
self.current_mode == "plan"
|
|
186
|
+
and target_mode == "agent"
|
|
187
|
+
and isinstance(plan_data, dict)
|
|
188
|
+
)
|
|
189
|
+
if (
|
|
190
|
+
not submitting_plan
|
|
191
|
+
and context.session
|
|
192
|
+
and hasattr(context.session, "switch_mode")
|
|
193
|
+
):
|
|
194
|
+
context.session.switch_mode(target_mode)
|
|
195
|
+
if context.tool_event_queue and not submitting_plan:
|
|
196
|
+
await context.tool_event_queue.put(
|
|
197
|
+
ModeChangeEvent(mode=target_mode, reason=explanation)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if target_mode == "agent" and plan_data:
|
|
201
|
+
return ToolResult(
|
|
202
|
+
result_for_model=(
|
|
203
|
+
"Execution plan submitted for user review; the session remains "
|
|
204
|
+
"in read-only plan mode. "
|
|
205
|
+
f"The plan has {len(plan_data.get('steps', []))} steps. "
|
|
206
|
+
f"Return control to the interface now so the user can choose whether to "
|
|
207
|
+
f"execute, revise, or cancel it. Do not call more tools in this turn."
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return ToolResult(
|
|
212
|
+
result_for_model=f"Mode switch to '{target_mode}' requested. {explanation}",
|
|
213
|
+
)
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Agent Team tools — LLM-callable tools for managing teams.
|
|
2
|
+
|
|
3
|
+
These tools allow the lead agent to create teams, spawn teammates,
|
|
4
|
+
send messages, manage tasks, and shut down teams through tool calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from crabcode_core.team.models import TeammateRole
|
|
12
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TeamCreateTool(Tool):
|
|
16
|
+
name = "TeamCreate"
|
|
17
|
+
description = "Create a new agent team for coordinated multi-agent work."
|
|
18
|
+
is_read_only = False
|
|
19
|
+
is_concurrency_safe = True
|
|
20
|
+
input_schema = {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"name": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"description": "Name for the team (also used as team ID).",
|
|
27
|
+
},
|
|
28
|
+
"max_teammates": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"description": "Maximum number of teammates (default: 8).",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
"required": ["name"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
37
|
+
manager = context.team_manager
|
|
38
|
+
if not manager:
|
|
39
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
40
|
+
try:
|
|
41
|
+
team_id = await manager.create_team(
|
|
42
|
+
tool_input["name"],
|
|
43
|
+
max_teammates=tool_input.get("max_teammates"),
|
|
44
|
+
)
|
|
45
|
+
return ToolResult(
|
|
46
|
+
data={"team_id": team_id},
|
|
47
|
+
result_for_model=f"Team created: {team_id}",
|
|
48
|
+
)
|
|
49
|
+
except (ValueError, RuntimeError) as exc:
|
|
50
|
+
return ToolResult(result_for_model=f"Error: {exc}", is_error=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TeamSpawnTool(Tool):
|
|
54
|
+
name = "TeamSpawn"
|
|
55
|
+
description = "Spawn a teammate agent within a team."
|
|
56
|
+
is_read_only = False
|
|
57
|
+
is_concurrency_safe = True
|
|
58
|
+
input_schema = {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"team_id": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "The team to add the teammate to.",
|
|
64
|
+
},
|
|
65
|
+
"role": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["lead", "worker", "researcher", "reviewer"],
|
|
68
|
+
"description": "Role for the teammate (default: worker).",
|
|
69
|
+
},
|
|
70
|
+
"prompt": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"minLength": 1,
|
|
73
|
+
"description": "The task/prompt for the teammate agent.",
|
|
74
|
+
},
|
|
75
|
+
"name": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Optional display name for the teammate.",
|
|
78
|
+
},
|
|
79
|
+
"model_profile": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": (
|
|
82
|
+
"Optional named model profile configured under settings.models "
|
|
83
|
+
"(enables multi-model teams). Do not pass a provider model ID."
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"required": ["team_id", "prompt"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
91
|
+
manager = context.team_manager
|
|
92
|
+
if not manager:
|
|
93
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
94
|
+
try:
|
|
95
|
+
agent_id = await manager.add_teammate(
|
|
96
|
+
tool_input["team_id"],
|
|
97
|
+
role=TeammateRole(tool_input.get("role", "worker")),
|
|
98
|
+
prompt=tool_input["prompt"],
|
|
99
|
+
name=tool_input.get("name"),
|
|
100
|
+
model_profile=tool_input.get("model_profile"),
|
|
101
|
+
)
|
|
102
|
+
return ToolResult(
|
|
103
|
+
data={"agent_id": agent_id},
|
|
104
|
+
result_for_model=f"Teammate spawned: {agent_id}",
|
|
105
|
+
)
|
|
106
|
+
except (ValueError, RuntimeError) as exc:
|
|
107
|
+
return ToolResult(result_for_model=f"Error: {exc}", is_error=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TeamMessageTool(Tool):
|
|
111
|
+
name = "TeamMessage"
|
|
112
|
+
description = "Send a message to a specific teammate in your team."
|
|
113
|
+
is_read_only = False
|
|
114
|
+
is_concurrency_safe = True
|
|
115
|
+
input_schema = {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"team_id": {"type": "string", "description": "Your team ID."},
|
|
119
|
+
"to": {"type": "string", "description": "Agent ID of the recipient."},
|
|
120
|
+
"text": {"type": "string", "minLength": 1, "description": "Message content."},
|
|
121
|
+
},
|
|
122
|
+
"required": ["team_id", "to", "text"],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
126
|
+
manager = context.team_manager
|
|
127
|
+
if not manager:
|
|
128
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
129
|
+
from_agent = context.agent_id or ""
|
|
130
|
+
msg = await manager.send_message(
|
|
131
|
+
tool_input["team_id"],
|
|
132
|
+
from_agent=from_agent,
|
|
133
|
+
to_agent=tool_input["to"],
|
|
134
|
+
text=tool_input["text"],
|
|
135
|
+
)
|
|
136
|
+
if msg is None:
|
|
137
|
+
return ToolResult(result_for_model="Error: message delivery failed", is_error=True)
|
|
138
|
+
return ToolResult(
|
|
139
|
+
data={"message_id": msg.id, "delivered": True},
|
|
140
|
+
result_for_model=f"Message delivered to {tool_input['to'][:8]}",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TeamBroadcastTool(Tool):
|
|
145
|
+
name = "TeamBroadcast"
|
|
146
|
+
description = "Broadcast a message to all teammates in your team (except yourself)."
|
|
147
|
+
is_read_only = False
|
|
148
|
+
is_concurrency_safe = True
|
|
149
|
+
input_schema = {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"team_id": {"type": "string", "description": "Your team ID."},
|
|
153
|
+
"text": {"type": "string", "minLength": 1, "description": "Message content."},
|
|
154
|
+
},
|
|
155
|
+
"required": ["team_id", "text"],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
159
|
+
manager = context.team_manager
|
|
160
|
+
if not manager:
|
|
161
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
162
|
+
from_agent = context.agent_id or ""
|
|
163
|
+
messages = await manager.broadcast(
|
|
164
|
+
tool_input["team_id"],
|
|
165
|
+
from_agent=from_agent,
|
|
166
|
+
text=tool_input["text"],
|
|
167
|
+
)
|
|
168
|
+
return ToolResult(
|
|
169
|
+
data={"recipient_count": len(messages)},
|
|
170
|
+
result_for_model=f"Broadcast delivered to {len(messages)} teammate(s)",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TeamStatusTool(Tool):
|
|
175
|
+
name = "TeamStatus"
|
|
176
|
+
description = "View team status, teammate states, and task board summary."
|
|
177
|
+
is_read_only = True
|
|
178
|
+
is_concurrency_safe = True
|
|
179
|
+
input_schema = {
|
|
180
|
+
"type": "object",
|
|
181
|
+
"properties": {
|
|
182
|
+
"team_id": {"type": "string", "description": "Team ID to inspect."},
|
|
183
|
+
},
|
|
184
|
+
"required": ["team_id"],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
188
|
+
manager = context.team_manager
|
|
189
|
+
if not manager:
|
|
190
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
191
|
+
status = manager.get_team_status(tool_input["team_id"])
|
|
192
|
+
if not status:
|
|
193
|
+
return ToolResult(result_for_model=f"Error: team '{tool_input['team_id']}' not found", is_error=True)
|
|
194
|
+
|
|
195
|
+
lines = [
|
|
196
|
+
f"Team: {status['team_id']} State: {status['state']}",
|
|
197
|
+
f"Teammates: {status['teammate_count']}/{status['max_teammates']}",
|
|
198
|
+
]
|
|
199
|
+
for t in status["teammates"]:
|
|
200
|
+
name = t.get("name") or t["agent_id"][:8]
|
|
201
|
+
lines.append(f" {name} · {t['role']} · {t['state']}")
|
|
202
|
+
tasks = status["tasks"]
|
|
203
|
+
lines.append(
|
|
204
|
+
f"Tasks: {tasks['total']} total "
|
|
205
|
+
f"({tasks['pending']} pending, {tasks['claimed']} claimed, "
|
|
206
|
+
f"{tasks['completed']} done, {tasks['failed']} failed)"
|
|
207
|
+
)
|
|
208
|
+
return ToolResult(data=status, result_for_model="\n".join(lines))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TeamTaskAddTool(Tool):
|
|
212
|
+
name = "TeamTaskAdd"
|
|
213
|
+
description = "Add a task to the team's shared task board."
|
|
214
|
+
is_read_only = False
|
|
215
|
+
is_concurrency_safe = True
|
|
216
|
+
input_schema = {
|
|
217
|
+
"type": "object",
|
|
218
|
+
"properties": {
|
|
219
|
+
"team_id": {"type": "string", "description": "Team ID."},
|
|
220
|
+
"description": {"type": "string", "minLength": 1, "description": "Task description."},
|
|
221
|
+
},
|
|
222
|
+
"required": ["team_id", "description"],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
226
|
+
manager = context.team_manager
|
|
227
|
+
if not manager:
|
|
228
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
229
|
+
try:
|
|
230
|
+
task_id = await manager.add_task(tool_input["team_id"], tool_input["description"])
|
|
231
|
+
return ToolResult(
|
|
232
|
+
data={"task_id": task_id},
|
|
233
|
+
result_for_model=f"Task added: {task_id}",
|
|
234
|
+
)
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
return ToolResult(result_for_model=f"Error: {exc}", is_error=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TeamTaskClaimTool(Tool):
|
|
240
|
+
name = "TeamTaskClaim"
|
|
241
|
+
description = "Claim a pending task from the team's task board. Only one agent can claim each task."
|
|
242
|
+
is_read_only = False
|
|
243
|
+
is_concurrency_safe = True
|
|
244
|
+
input_schema = {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"team_id": {"type": "string", "description": "Team ID."},
|
|
248
|
+
"task_id": {"type": "string", "description": "Task ID to claim."},
|
|
249
|
+
},
|
|
250
|
+
"required": ["team_id", "task_id"],
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
254
|
+
manager = context.team_manager
|
|
255
|
+
if not manager:
|
|
256
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
257
|
+
agent_id = context.agent_id or ""
|
|
258
|
+
claimed = await manager.claim_task(tool_input["team_id"], tool_input["task_id"], agent_id)
|
|
259
|
+
if not claimed:
|
|
260
|
+
return ToolResult(
|
|
261
|
+
result_for_model="Error: task not found or already claimed",
|
|
262
|
+
is_error=True,
|
|
263
|
+
)
|
|
264
|
+
return ToolResult(
|
|
265
|
+
data={"task_id": tool_input["task_id"], "claimed": True},
|
|
266
|
+
result_for_model=f"Task claimed: {tool_input['task_id'][:8]}",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TeamTaskCompleteTool(Tool):
|
|
271
|
+
name = "TeamTaskComplete"
|
|
272
|
+
description = "Mark a claimed task as completed with a result."
|
|
273
|
+
is_read_only = False
|
|
274
|
+
is_concurrency_safe = True
|
|
275
|
+
input_schema = {
|
|
276
|
+
"type": "object",
|
|
277
|
+
"properties": {
|
|
278
|
+
"team_id": {"type": "string", "description": "Team ID."},
|
|
279
|
+
"task_id": {"type": "string", "description": "Task ID to complete."},
|
|
280
|
+
"result": {"type": "string", "description": "Result or summary of the completed task."},
|
|
281
|
+
},
|
|
282
|
+
"required": ["team_id", "task_id"],
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
286
|
+
manager = context.team_manager
|
|
287
|
+
if not manager:
|
|
288
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
289
|
+
completed = await manager.complete_task(
|
|
290
|
+
tool_input["team_id"],
|
|
291
|
+
tool_input["task_id"],
|
|
292
|
+
tool_input.get("result", ""),
|
|
293
|
+
context.agent_id or "",
|
|
294
|
+
)
|
|
295
|
+
if not completed:
|
|
296
|
+
return ToolResult(
|
|
297
|
+
result_for_model="Error: task not found or not in claimed state",
|
|
298
|
+
is_error=True,
|
|
299
|
+
)
|
|
300
|
+
return ToolResult(
|
|
301
|
+
data={"task_id": tool_input["task_id"], "completed": True},
|
|
302
|
+
result_for_model=f"Task completed: {tool_input['task_id'][:8]}",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TeamShutdownTool(Tool):
|
|
307
|
+
name = "TeamShutdown"
|
|
308
|
+
description = "Shut down a team: cancel all teammates and clean up resources."
|
|
309
|
+
is_read_only = False
|
|
310
|
+
is_concurrency_safe = True
|
|
311
|
+
input_schema = {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"properties": {
|
|
314
|
+
"team_id": {"type": "string", "description": "Team ID to shut down."},
|
|
315
|
+
},
|
|
316
|
+
"required": ["team_id"],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
320
|
+
manager = context.team_manager
|
|
321
|
+
if not manager:
|
|
322
|
+
return ToolResult(result_for_model="Error: team manager unavailable", is_error=True)
|
|
323
|
+
shutdown = await manager.shutdown_team(tool_input["team_id"])
|
|
324
|
+
if not shutdown:
|
|
325
|
+
return ToolResult(result_for_model=f"Error: team '{tool_input['team_id']}' not found", is_error=True)
|
|
326
|
+
return ToolResult(
|
|
327
|
+
data={"team_id": tool_input["team_id"], "shutdown": True},
|
|
328
|
+
result_for_model=f"Team shut down: {tool_input['team_id']}",
|
|
329
|
+
)
|