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,191 @@
1
+ """Core data models for Agent Teams.
2
+
3
+ Defines Team, Teammate, Message, Task and related types.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ def _now_iso() -> str:
17
+ return datetime.now(timezone.utc).isoformat()
18
+
19
+
20
+ def _new_id() -> str:
21
+ return str(uuid.uuid4())
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Enums
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ class TeamState(str, Enum):
30
+ FORMING = "forming"
31
+ ACTIVE = "active"
32
+ IDLE = "idle"
33
+ SHUTDOWN = "shutdown"
34
+
35
+
36
+ class TeammateRole(str, Enum):
37
+ LEAD = "lead"
38
+ WORKER = "worker"
39
+ RESEARCHER = "researcher"
40
+ REVIEWER = "reviewer"
41
+
42
+
43
+ class TeammateState(str, Enum):
44
+ IDLE = "idle"
45
+ BUSY = "busy"
46
+ READY = "ready"
47
+ CANCELLING = "cancelling"
48
+ SHUTDOWN = "shutdown"
49
+
50
+ @classmethod
51
+ def terminal_states(cls) -> set[TeammateState]:
52
+ return {cls.IDLE, cls.READY, cls.SHUTDOWN}
53
+
54
+ def can_transition_to(self, target: TeammateState) -> bool:
55
+ """Validate state machine transitions."""
56
+ _ALLOWED: dict[TeammateState, set[TeammateState]] = {
57
+ TeammateState.IDLE: {TeammateState.BUSY, TeammateState.SHUTDOWN},
58
+ TeammateState.BUSY: {TeammateState.READY, TeammateState.CANCELLING, TeammateState.SHUTDOWN},
59
+ TeammateState.READY: {TeammateState.BUSY, TeammateState.SHUTDOWN},
60
+ TeammateState.CANCELLING: {TeammateState.READY, TeammateState.SHUTDOWN},
61
+ TeammateState.SHUTDOWN: set(), # terminal
62
+ }
63
+ return target in _ALLOWED.get(self, set())
64
+
65
+
66
+ class TaskStatus(str, Enum):
67
+ PENDING = "pending"
68
+ CLAIMED = "claimed"
69
+ COMPLETED = "completed"
70
+ FAILED = "failed"
71
+
72
+
73
+ class BridgePolicy(str, Enum):
74
+ ALLOW_ALL = "allow_all"
75
+ ALLOW_TAGGED = "allow_tagged"
76
+ DENY = "deny"
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Models
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ class TeamConfig(BaseModel):
85
+ """Configuration for a team."""
86
+
87
+ name: str = Field(min_length=1)
88
+ max_teammates: int = Field(default=8, gt=0)
89
+ inbox_dir: str | None = None # override default inbox storage path
90
+ backpressure_queue_size: int = Field(default=100, gt=0)
91
+ max_message_size_bytes: int = Field(default=10_000, gt=0) # 10KB per message
92
+ bridge_policy: BridgePolicy = BridgePolicy.DENY
93
+
94
+
95
+ class TeammateInfo(BaseModel):
96
+ """Information about a teammate within a team."""
97
+
98
+ agent_id: str
99
+ role: TeammateRole = TeammateRole.WORKER
100
+ state: TeammateState = TeammateState.IDLE
101
+ model_profile: str | None = None
102
+ name: str | None = None
103
+ joined_at: str = Field(default_factory=_now_iso)
104
+ updated_at: str = Field(default_factory=_now_iso)
105
+
106
+ def transition_to(
107
+ self,
108
+ target: TeammateState,
109
+ *,
110
+ guard: bool = False,
111
+ force: bool = False,
112
+ ) -> bool:
113
+ """Attempt a state transition.
114
+
115
+ Args:
116
+ guard: If True, skip if already in a terminal state (prevents
117
+ race conditions during cleanup).
118
+ force: If True, bypass validation entirely (used in recovery).
119
+ """
120
+ if guard and self.state in TeammateState.terminal_states():
121
+ return False
122
+ if force or self.state.can_transition_to(target):
123
+ self.state = target
124
+ self.updated_at = _now_iso()
125
+ return True
126
+ return False
127
+
128
+
129
+ class TeamMessage(BaseModel):
130
+ """A message between teammates."""
131
+
132
+ id: str = Field(default_factory=_new_id)
133
+ from_agent: str = "" # agent_id of sender
134
+ to_agent: str = "" # agent_id of recipient (empty = broadcast)
135
+ text: str = ""
136
+ timestamp: str = Field(default_factory=_now_iso)
137
+ read: bool = False
138
+ msg_type: str = "text" # text | task | system | delivery_receipt
139
+
140
+ def size_bytes(self) -> int:
141
+ return len(self.text.encode("utf-8"))
142
+
143
+
144
+ class TaskItem(BaseModel):
145
+ """A task on the shared task board."""
146
+
147
+ id: str = Field(default_factory=_new_id)
148
+ description: str = ""
149
+ assignee: str | None = None # agent_id
150
+ status: TaskStatus = TaskStatus.PENDING
151
+ result: str = ""
152
+ created_at: str = Field(default_factory=_now_iso)
153
+ updated_at: str = Field(default_factory=_now_iso)
154
+
155
+ def claim(self, agent_id: str) -> bool:
156
+ """Atomically claim this task if it's still pending."""
157
+ if self.status != TaskStatus.PENDING:
158
+ return False
159
+ self.status = TaskStatus.CLAIMED
160
+ self.assignee = agent_id
161
+ self.updated_at = _now_iso()
162
+ return True
163
+
164
+ def complete(self, result: str = "") -> bool:
165
+ if self.status != TaskStatus.CLAIMED:
166
+ return False
167
+ self.status = TaskStatus.COMPLETED
168
+ self.result = result
169
+ self.updated_at = _now_iso()
170
+ return True
171
+
172
+ def fail(self, reason: str = "") -> bool:
173
+ if self.status != TaskStatus.CLAIMED:
174
+ return False
175
+ self.status = TaskStatus.FAILED
176
+ self.result = reason
177
+ self.updated_at = _now_iso()
178
+ return True
179
+
180
+
181
+ class CrossTeamMessage(BaseModel):
182
+ """A message sent between teams via a bridge."""
183
+
184
+ id: str = Field(default_factory=_new_id)
185
+ from_team: str = ""
186
+ from_agent: str = ""
187
+ to_team: str = ""
188
+ to_agent: str = "" # empty = broadcast within target team
189
+ text: str = ""
190
+ timestamp: str = Field(default_factory=_now_iso)
191
+ bridge_policy: BridgePolicy = BridgePolicy.ALLOW_TAGGED
@@ -0,0 +1,192 @@
1
+ """Crash recovery for Agent Teams.
2
+
3
+ When a long-lived process has stale teammate state (for example, a teammate
4
+ marked as "busy" that is no longer running), it can be cleaned up explicitly
5
+ through this module. Team state is currently in memory and this helper is not
6
+ wired into gateway startup, so it does not provide cross-process/server-restart
7
+ recovery by itself.
8
+
9
+ Key design decision: NO automatic restart of interrupted teammates.
10
+ This prevents runaway agents burning API credits after a crash.
11
+ The human must re-engage them manually.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from typing import Awaitable, Callable
18
+
19
+ from crabcode_core.logging_utils import get_logger
20
+ from crabcode_core.team.manager import TeamManager
21
+ from crabcode_core.team.models import TeammateState
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ # Cancellation retry settings
26
+ _CANCEL_RETRIES = 3
27
+ _CANCEL_RETRY_DELAY_S = 0.12 # 120ms
28
+
29
+
30
+ class RecoveryInfo:
31
+ """Result of a recovery operation."""
32
+
33
+ def __init__(
34
+ self,
35
+ team_id: str,
36
+ agent_id: str,
37
+ old_state: str,
38
+ new_state: str,
39
+ ) -> None:
40
+ self.team_id = team_id
41
+ self.agent_id = agent_id
42
+ self.old_state = old_state
43
+ self.new_state = new_state
44
+
45
+ def __repr__(self) -> str:
46
+ return (
47
+ f"RecoveryInfo(team={self.team_id}, agent={self.agent_id[:8]}, "
48
+ f"{self.old_state} -> {self.new_state})"
49
+ )
50
+
51
+
52
+ async def recover_teams(
53
+ team_manager: TeamManager,
54
+ *,
55
+ inject_fn: Callable[[str, str], Awaitable[None]] | None = None,
56
+ ) -> list[RecoveryInfo]:
57
+ """Recover stale teammates in the supplied in-process manager.
58
+
59
+ This is not a server-startup hook: a newly-created manager has no persisted
60
+ team manifest to inspect, so callers must reconstruct the runtime first.
61
+
62
+ Recovery sequence:
63
+ 1. Find all busy teammates across all teams
64
+ 2. Force-transition them from busy -> ready
65
+ 3. Inject a notification into the lead session
66
+ 4. Do NOT auto-restart agents (prevents runaway)
67
+
68
+ Args:
69
+ team_manager: The TeamManager to recover.
70
+ inject_fn: Optional async function (agent_id, notification_text) to
71
+ inject recovery notifications into sessions.
72
+
73
+ Returns:
74
+ List of RecoveryInfo for interrupted teammates.
75
+ """
76
+ interrupted = await team_manager.recover()
77
+
78
+ if not interrupted:
79
+ return []
80
+
81
+ recovery_infos: list[RecoveryInfo] = []
82
+ recovered_states = getattr(team_manager, "_last_recovered_states", {})
83
+ for item in interrupted:
84
+ state_key = (item.get("team_id", ""), item.get("agent_id", ""))
85
+ info = RecoveryInfo(
86
+ team_id=item["team_id"],
87
+ agent_id=item["agent_id"],
88
+ # ``old_state`` was not part of the original TeamManager return
89
+ # shape. Keep the BUSY default for third-party managers while
90
+ # preserving the more useful state when the built-in manager
91
+ # reports a stale CANCELLING teammate.
92
+ old_state=item.get(
93
+ "old_state",
94
+ recovered_states.get(state_key, TeammateState.BUSY.value),
95
+ ),
96
+ new_state=TeammateState.READY.value,
97
+ )
98
+ recovery_infos.append(info)
99
+
100
+ # Inject notification into the lead session
101
+ if inject_fn:
102
+ team_ids = sorted({item["team_id"] for item in interrupted})
103
+ for team_id in team_ids:
104
+ team_agents = [
105
+ item["agent_id"][:8]
106
+ for item in interrupted
107
+ if item["team_id"] == team_id
108
+ ]
109
+ notification = (
110
+ f"[System]: Server was restarted. The following teammates in "
111
+ f'team "{team_id}" were interrupted and need to be resumed: '
112
+ f"{', '.join(team_agents)}. "
113
+ f"Use TeamMessage or TeamBroadcast to tell them to continue their work."
114
+ )
115
+ # Prefer an explicitly designated lead. Older teams may not have
116
+ # one (the main session is not currently represented as a
117
+ # teammate), so retain the historical first-member fallback.
118
+ get_lead_agent = getattr(team_manager, "get_lead_agent", None)
119
+ lead_id = (
120
+ get_lead_agent(team_id)
121
+ if callable(get_lead_agent)
122
+ else None
123
+ )
124
+ if lead_id is None:
125
+ lead_id = next(
126
+ (item["agent_id"] for item in interrupted if item["team_id"] == team_id),
127
+ None,
128
+ )
129
+ if lead_id is None:
130
+ continue
131
+ try:
132
+ await inject_fn(lead_id, notification)
133
+ except Exception:
134
+ logger.warning(
135
+ "Failed to inject recovery notification for %s",
136
+ lead_id,
137
+ exc_info=True,
138
+ )
139
+
140
+ logger.info("Recovered %d interrupted teammates", len(recovery_infos))
141
+ return recovery_infos
142
+
143
+
144
+ async def cancel_agent_with_retry(
145
+ cancel_fn: Callable[[str], Awaitable[bool]],
146
+ agent_id: str,
147
+ *,
148
+ retries: int = _CANCEL_RETRIES,
149
+ delay: float = _CANCEL_RETRY_DELAY_S,
150
+ force_fn: Callable[[str], Awaitable[None]] | None = None,
151
+ ) -> bool:
152
+ """Cancel an agent with retry. If it hasn't stopped after retries,
153
+ force-transition as a safety net.
154
+
155
+ Args:
156
+ cancel_fn: Async function that cancels an agent and returns True if successful.
157
+ agent_id: The agent to cancel.
158
+ retries: Number of cancellation attempts.
159
+ delay: Delay between retries in seconds.
160
+ """
161
+ for attempt in range(retries):
162
+ try:
163
+ cancelled = await cancel_fn(agent_id)
164
+ if cancelled:
165
+ return True
166
+ except Exception:
167
+ logger.debug("Cancel attempt %d failed for %s", attempt + 1, agent_id, exc_info=True)
168
+
169
+ if attempt < retries - 1:
170
+ await asyncio.sleep(delay)
171
+
172
+ # Give the owner a chance to force-transition stale state. The old
173
+ # implementation only logged and returned False, leaving a teammate stuck
174
+ # in cancelling/busy forever despite its docstring promising a safety net.
175
+ if force_fn is not None:
176
+ try:
177
+ await force_fn(agent_id)
178
+ except Exception:
179
+ logger.debug("Force-transition callback failed for %s", agent_id, exc_info=True)
180
+ if force_fn is not None:
181
+ logger.warning(
182
+ "Force-transitioning agent %s after %d failed cancel attempts",
183
+ agent_id,
184
+ retries,
185
+ )
186
+ else:
187
+ logger.warning(
188
+ "Unable to cancel agent %s after %d attempts; no force callback configured",
189
+ agent_id,
190
+ retries,
191
+ )
192
+ return False
@@ -0,0 +1,52 @@
1
+ """UTF-8 text I/O that preserves existing file newline conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import codecs
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Utf8Text:
12
+ text: str
13
+ newline: str | None
14
+ has_bom: bool
15
+
16
+
17
+ def read_utf8_text(path: Path) -> Utf8Text:
18
+ raw = path.read_bytes()
19
+ has_bom = raw.startswith(codecs.BOM_UTF8)
20
+ if has_bom:
21
+ raw = raw[len(codecs.BOM_UTF8):]
22
+ text = raw.decode("utf-8")
23
+ return Utf8Text(text=text, newline=_detect_newline(text), has_bom=has_bom)
24
+
25
+
26
+ def write_utf8_text(
27
+ path: Path,
28
+ text: str,
29
+ *,
30
+ newline: str | None = None,
31
+ has_bom: bool = False,
32
+ ) -> int:
33
+ if newline is not None:
34
+ text = normalize_newlines(text, newline)
35
+ raw = text.encode("utf-8")
36
+ if has_bom:
37
+ raw = codecs.BOM_UTF8 + raw
38
+ return path.write_bytes(raw)
39
+
40
+
41
+ def normalize_newlines(text: str, newline: str) -> str:
42
+ normalized = text.replace("\r\n", "\n").replace("\r", "\n")
43
+ return normalized if newline == "\n" else normalized.replace("\n", newline)
44
+
45
+
46
+ def _detect_newline(text: str) -> str | None:
47
+ crlf = text.count("\r\n")
48
+ lf = text.count("\n") - crlf
49
+ cr = text.count("\r") - crlf
50
+ counts = [(crlf, "\r\n"), (lf, "\n"), (cr, "\r")]
51
+ count, newline = max(counts, key=lambda item: item[0])
52
+ return newline if count else None
@@ -0,0 +1,112 @@
1
+ """Tool system - built-in tools and tool registry."""
2
+
3
+ from crabcode_core.types.tool import Tool, ToolResult, ToolContext
4
+
5
+
6
+ def get_default_tools() -> list[Tool]:
7
+ """Return the default set of built-in tools."""
8
+ from crabcode_core.tools.agent import (
9
+ AgentCancelTool,
10
+ AgentSendInputTool,
11
+ AgentStatusTool,
12
+ AgentWaitTool,
13
+ )
14
+ from crabcode_core.tools.ask_user import AskUserTool
15
+ from crabcode_core.tools.apply_patch import ApplyPatchTool
16
+ from crabcode_core.tools.checklist import ChecklistTool
17
+ from crabcode_core.tools.checkpoint import CheckpointTool
18
+ from crabcode_core.tools.computer_use import ComputerUseTool
19
+ from crabcode_core.tools.bash import BashTool
20
+ from crabcode_core.tools.browser import BrowserTool
21
+ from crabcode_core.tools.image import ImageTool
22
+ from crabcode_core.tools.image_generate import ImageGenerateTool
23
+ from crabcode_core.tools.file_read import FileReadTool
24
+ from crabcode_core.tools.file_edit import FileEditTool
25
+ from crabcode_core.tools.file_write import FileWriteTool
26
+ from crabcode_core.tools.grep import GrepTool
27
+ from crabcode_core.tools.goal import CreateGoalTool, GetGoalTool, UpdateGoalTool
28
+ from crabcode_core.tools.glob import GlobTool
29
+ from crabcode_core.tools.lint import LintTool
30
+ from crabcode_core.tools.memory import MemoryTool
31
+ from crabcode_core.tools.monitor import MonitorManager, MonitorTool, TaskListTool, TaskStopTool
32
+ from crabcode_core.tools.peer import ListAgentsTool, SendMessageTool
33
+ from crabcode_core.tools.revert import RevertTool
34
+ from crabcode_core.tools.switch_mode import SwitchModeTool
35
+ from crabcode_core.tools.schedule import (
36
+ ScheduleCancelTool,
37
+ ScheduleCreateTool,
38
+ ScheduleListTool,
39
+ SchedulePauseTool,
40
+ ScheduleResumeTool,
41
+ ScheduleRunTool,
42
+ ScheduleStatusTool,
43
+ )
44
+ from crabcode_core.tools.team import (
45
+ TeamBroadcastTool,
46
+ TeamCreateTool,
47
+ TeamMessageTool,
48
+ TeamShutdownTool,
49
+ TeamSpawnTool,
50
+ TeamStatusTool,
51
+ TeamTaskAddTool,
52
+ TeamTaskClaimTool,
53
+ TeamTaskCompleteTool,
54
+ )
55
+ from crabcode_core.tools.web_search import WebSearchTool
56
+ from crabcode_core.tools.tool_search import ToolSearchTool
57
+
58
+ monitor_manager = MonitorManager()
59
+
60
+ return [
61
+ ToolSearchTool(),
62
+ BashTool(),
63
+ FileReadTool(),
64
+ ApplyPatchTool(),
65
+ FileEditTool(),
66
+ FileWriteTool(),
67
+ GrepTool(),
68
+ GlobTool(),
69
+ WebSearchTool(),
70
+ BrowserTool(),
71
+ ComputerUseTool(),
72
+ ImageTool(),
73
+ ImageGenerateTool(),
74
+ LintTool(),
75
+ MemoryTool(),
76
+ MonitorTool(monitor_manager),
77
+ TaskListTool(monitor_manager),
78
+ TaskStopTool(monitor_manager),
79
+ AgentStatusTool(),
80
+ AgentWaitTool(),
81
+ AgentCancelTool(),
82
+ AgentSendInputTool(),
83
+ ListAgentsTool(),
84
+ SendMessageTool(),
85
+ AskUserTool(),
86
+ CheckpointTool(),
87
+ ChecklistTool(),
88
+ CreateGoalTool(),
89
+ GetGoalTool(),
90
+ UpdateGoalTool(),
91
+ SwitchModeTool(),
92
+ TeamCreateTool(),
93
+ TeamSpawnTool(),
94
+ TeamMessageTool(),
95
+ TeamBroadcastTool(),
96
+ TeamStatusTool(),
97
+ TeamTaskAddTool(),
98
+ TeamTaskClaimTool(),
99
+ TeamTaskCompleteTool(),
100
+ TeamShutdownTool(),
101
+ RevertTool(),
102
+ ScheduleCreateTool(),
103
+ ScheduleListTool(),
104
+ ScheduleCancelTool(),
105
+ ScheduleStatusTool(),
106
+ SchedulePauseTool(),
107
+ ScheduleResumeTool(),
108
+ ScheduleRunTool(),
109
+ ]
110
+
111
+
112
+ __all__ = ["Tool", "ToolResult", "ToolContext", "get_default_tools"]
@@ -0,0 +1,39 @@
1
+ """Helpers for normalizing model-provided tool arguments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from crabcode_core.types.message import Message, MessageRole, TextBlock
8
+
9
+
10
+ def first_non_empty_str(tool_input: dict[str, Any], keys: tuple[str, ...]) -> str:
11
+ """Return the first stripped non-empty string among the given keys."""
12
+ for key in keys:
13
+ raw = tool_input.get(key)
14
+ if isinstance(raw, str) and raw.strip():
15
+ return raw.strip()
16
+ return ""
17
+
18
+
19
+ def latest_user_text_for_agent_fallback(messages: list[Message]) -> str:
20
+ """Text from the most recent user message that contains user-typed text (TextBlock).
21
+
22
+ Skips user messages that only carry tool results, so a missing ``prompt`` on
23
+ Agent can still map to the user's actual question when the model omits args.
24
+ """
25
+ for msg in reversed(messages):
26
+ if msg.role != MessageRole.USER:
27
+ continue
28
+ if isinstance(msg.content, str):
29
+ t = msg.content.strip()
30
+ if t:
31
+ return t
32
+ parts: list[str] = []
33
+ for block in msg.content:
34
+ if isinstance(block, TextBlock):
35
+ parts.append(block.text)
36
+ text = "".join(parts).strip()
37
+ if text:
38
+ return text
39
+ return ""