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,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
|
crabcode_core/text_io.py
ADDED
|
@@ -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 ""
|