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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. 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
+ )