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,130 @@
1
+ """Normalize model-generated tool inputs against their JSON Schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ _CAMEL_BOUNDARY = re.compile(
11
+ r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])"
12
+ )
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ToolInputCorrection:
17
+ path: str
18
+ received: str
19
+ expected: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ToolInputNormalization:
24
+ value: dict[str, Any]
25
+ corrections: tuple[ToolInputCorrection, ...] = ()
26
+ error: str | None = None
27
+
28
+
29
+ def _to_snake_case(key: str) -> str:
30
+ return _CAMEL_BOUNDARY.sub("_", key).lower()
31
+
32
+
33
+ def _display_path(parent: str, key: str) -> str:
34
+ return f"{parent}.{key}" if parent else key
35
+
36
+
37
+ def _normalize_value(
38
+ value: Any,
39
+ schema: Any,
40
+ path: str,
41
+ ) -> tuple[Any, list[ToolInputCorrection], str | None]:
42
+ if not isinstance(schema, dict):
43
+ return value, [], None
44
+
45
+ if isinstance(value, list):
46
+ item_schema = schema.get("items")
47
+ if not isinstance(item_schema, dict):
48
+ return value, [], None
49
+ normalized_items = []
50
+ corrections: list[ToolInputCorrection] = []
51
+ for index, item in enumerate(value):
52
+ normalized, nested, error = _normalize_value(
53
+ item,
54
+ item_schema,
55
+ f"{path}[{index}]",
56
+ )
57
+ if error:
58
+ return value, corrections, error
59
+ normalized_items.append(normalized)
60
+ corrections.extend(nested)
61
+ return normalized_items, corrections, None
62
+
63
+ if not isinstance(value, dict):
64
+ return value, [], None
65
+
66
+ properties = schema.get("properties")
67
+ if not isinstance(properties, dict):
68
+ return value, [], None
69
+
70
+ normalized: dict[str, Any] = {
71
+ key: item for key, item in value.items() if key in properties
72
+ }
73
+ corrections: list[ToolInputCorrection] = []
74
+
75
+ for received, item in value.items():
76
+ if received in properties:
77
+ continue
78
+ expected = _to_snake_case(received)
79
+ if expected == received or expected not in properties:
80
+ normalized[received] = item
81
+ continue
82
+
83
+ received_path = _display_path(path, received)
84
+ expected_path = _display_path(path, expected)
85
+ if expected in normalized and normalized[expected] != item:
86
+ return value, corrections, (
87
+ f'Received "{received_path}"; did you mean "{expected_path}"? '
88
+ f'Both "{received_path}" and "{expected_path}" were provided with '
89
+ "different values, so the tool input could not be normalized unambiguously."
90
+ )
91
+
92
+ normalized.setdefault(expected, item)
93
+ corrections.append(
94
+ ToolInputCorrection(
95
+ path=received_path,
96
+ received=received,
97
+ expected=expected,
98
+ )
99
+ )
100
+
101
+ for key, item in list(normalized.items()):
102
+ property_schema = properties.get(key)
103
+ if not isinstance(property_schema, dict):
104
+ continue
105
+ normalized_item, nested, error = _normalize_value(
106
+ item,
107
+ property_schema,
108
+ _display_path(path, key),
109
+ )
110
+ if error:
111
+ return value, corrections, error
112
+ normalized[key] = normalized_item
113
+ corrections.extend(nested)
114
+
115
+ return normalized, corrections, None
116
+
117
+
118
+ def normalize_tool_input(
119
+ tool_input: dict[str, Any],
120
+ input_schema: dict[str, Any],
121
+ ) -> ToolInputNormalization:
122
+ """Normalize unambiguous camelCase keys declared as snake_case by a schema."""
123
+ value, corrections, error = _normalize_value(tool_input, input_schema, "")
124
+ if not isinstance(value, dict):
125
+ value = tool_input
126
+ return ToolInputNormalization(
127
+ value=value,
128
+ corrections=tuple(corrections),
129
+ error=error,
130
+ )
@@ -0,0 +1,413 @@
1
+ """Agent tools — managed sub-agents for parallel/isolated work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from crabcode_core.agent_manager import AgentManager
9
+ from crabcode_core.tools._input_helpers import (
10
+ first_non_empty_str,
11
+ latest_user_text_for_agent_fallback,
12
+ )
13
+ from crabcode_core.types.config import AgentSettings
14
+ from crabcode_core.types.tool import Tool, ToolContext, ToolResult
15
+
16
+
17
+ class AgentTool(Tool):
18
+ name = "Agent"
19
+ description = (
20
+ "Spawn a sub-agent for parallel or isolated tasks. Agents run in the "
21
+ "background by default and report back through a completion notification."
22
+ )
23
+ is_read_only = False
24
+ is_concurrency_safe = True
25
+ input_schema = {
26
+ "type": "object",
27
+ "properties": {
28
+ "prompt": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "The task for the sub-agent to perform.",
32
+ },
33
+ "subagent_type": {
34
+ "type": "string",
35
+ "description": "Type of agent to spawn (e.g., 'explore', 'generalPurpose').",
36
+ "enum": ["explore", "generalPurpose"],
37
+ },
38
+ "description": {
39
+ "type": "string",
40
+ "description": "A short description used to identify the task.",
41
+ },
42
+ "name": {
43
+ "type": "string",
44
+ "description": "Optional stable name for the sub-agent.",
45
+ },
46
+ "model_profile": {
47
+ "type": "string",
48
+ "description": (
49
+ "Optional named model profile configured under settings.models. "
50
+ "Do not pass a provider model ID."
51
+ ),
52
+ },
53
+ "run_in_background": {
54
+ "type": "boolean",
55
+ "description": (
56
+ "Run asynchronously and report back through a task notification. "
57
+ "Defaults to true; set false only when the result is required "
58
+ "before this turn can continue."
59
+ ),
60
+ "default": True,
61
+ },
62
+ },
63
+ "required": ["prompt"],
64
+ }
65
+
66
+ def __init__(
67
+ self,
68
+ manager: AgentManager | None = None,
69
+ settings: AgentSettings | None = None,
70
+ max_turns: int = 10,
71
+ timeout: int = 300,
72
+ max_output_chars: int = 12_000,
73
+ max_display_lines: int = 120,
74
+ ):
75
+ self._manager = manager
76
+ self._settings = settings or AgentSettings()
77
+ self._max_turns = max_turns
78
+ self._timeout = timeout
79
+ self._max_output_chars = max_output_chars
80
+ self._max_display_lines = max_display_lines
81
+
82
+ async def get_prompt(self, **kwargs: Any) -> str:
83
+ return (
84
+ "Launch a sub-agent to handle complex, multi-step tasks "
85
+ "autonomously. Subagents are valuable for parallelizing "
86
+ "independent queries or for protecting the main context "
87
+ "window from excessive results.\n\n"
88
+ "Available types:\n"
89
+ "- 'explore': Fast agent for codebase exploration — finding "
90
+ "files, searching code, answering questions about the codebase.\n"
91
+ "- 'generalPurpose': For complex multi-step tasks that need "
92
+ "searching, reading, and executing commands.\n\n"
93
+ "When to use:\n"
94
+ "- Broadly exploring the codebase for context on a large task\n"
95
+ "- Parallelizing independent research queries\n"
96
+ "- Tasks that may produce large outputs\n\n"
97
+ "Execution mode:\n"
98
+ "- Agents run in the background by default. The tool returns an agent ID "
99
+ "immediately, and the result arrives later as a task notification.\n"
100
+ "- Set run_in_background=false only when this turn cannot continue without "
101
+ "the result. Do not poll a background agent; its completion automatically "
102
+ "resumes the parent conversation.\n\n"
103
+ "When NOT to use:\n"
104
+ "- Simple, single-step tasks — just call the tools directly\n"
105
+ "- Searching for a specific file or class — use Glob or Grep\n\n"
106
+ "IMPORTANT: Avoid duplicating work that subagents are already "
107
+ "doing. If you delegate research to a subagent, do not also "
108
+ "perform the same searches yourself."
109
+ )
110
+
111
+ async def call(
112
+ self,
113
+ tool_input: dict[str, Any],
114
+ context: ToolContext,
115
+ ) -> ToolResult:
116
+ prompt = first_non_empty_str(
117
+ tool_input,
118
+ ("prompt", "task", "instruction", "query", "message", "description"),
119
+ )
120
+ if not prompt:
121
+ prompt = latest_user_text_for_agent_fallback(context.messages)
122
+
123
+ if not prompt:
124
+ return ToolResult(
125
+ result_for_model=(
126
+ "Error: prompt is required. Pass a non-empty \"prompt\" string "
127
+ "describing the sub-agent's task (or use field aliases: task, "
128
+ "instruction, query, message, description)."
129
+ ),
130
+ is_error=True,
131
+ )
132
+
133
+ manager = context.agent_manager or self._manager
134
+ if not manager:
135
+ return ToolResult(
136
+ result_for_model="Error: AgentTool requires an agent manager",
137
+ is_error=True,
138
+ )
139
+
140
+ agent_id: str | None = None
141
+ # Claude Code treats an omitted/null value as the background default;
142
+ # only an explicit false requests synchronous execution.
143
+ run_in_background = tool_input.get("run_in_background") is not False
144
+ title = first_non_empty_str(tool_input, ("description", "name"))
145
+ try:
146
+ agent_id = await manager.spawn_agent(
147
+ prompt=prompt,
148
+ subagent_type=tool_input.get("subagent_type", "generalPurpose"),
149
+ name=title,
150
+ model_profile=tool_input.get("model_profile"),
151
+ parent_agent_id=context.agent_id,
152
+ parent_tool_use_id=context.tool_use_id,
153
+ depth=context.agent_depth + 1,
154
+ callback=run_in_background,
155
+ )
156
+ if run_in_background:
157
+ snapshot = manager.get_agent(agent_id)
158
+ description = snapshot.title if snapshot is not None else (title or prompt[:80])
159
+ output_file = snapshot.transcript_path if snapshot is not None else None
160
+ resolved_model = snapshot.model if snapshot is not None else None
161
+ data = {
162
+ "status": "async_launched",
163
+ "isAsync": True,
164
+ "agentId": agent_id,
165
+ "description": description,
166
+ "resolvedModel": resolved_model,
167
+ "prompt": prompt,
168
+ "outputFile": output_file,
169
+ "canReadOutputFile": bool(output_file),
170
+ }
171
+ lines = [
172
+ "status: async_launched",
173
+ f"agentId: {agent_id}",
174
+ f"description: {description}",
175
+ ]
176
+ if resolved_model:
177
+ lines.append(f"resolvedModel: {resolved_model}")
178
+ if output_file:
179
+ lines.append(f"outputFile: {output_file}")
180
+ lines.append(
181
+ "The result will arrive automatically as a task notification; "
182
+ "do not poll for completion."
183
+ )
184
+ return ToolResult(data=data, result_for_model="\n".join(lines))
185
+ snapshot = await manager.wait_agent(agent_id, timeout_ms=self._timeout * 1000)
186
+ except asyncio.TimeoutError:
187
+ return ToolResult(
188
+ result_for_model=f"status: timed_out\nresult:\nSub-agent timed out after {self._timeout}s",
189
+ is_error=True,
190
+ )
191
+ except asyncio.CancelledError:
192
+ if agent_id:
193
+ await manager.cancel_agent(agent_id)
194
+ raise
195
+ except ValueError as exc:
196
+ return ToolResult(result_for_model=f"Error: {exc}", is_error=True)
197
+
198
+ if snapshot is None:
199
+ return ToolResult(
200
+ result_for_model="status: timeout\nresult:\nSub-agent wait timed out.",
201
+ is_error=True,
202
+ )
203
+
204
+ result = AgentManager.format_snapshot(
205
+ snapshot,
206
+ max_result_chars=self._max_output_chars,
207
+ )
208
+ display = result
209
+ lines = display.split("\n")
210
+ if len(lines) > self._max_display_lines:
211
+ kept = "\n".join(lines[:self._max_display_lines])
212
+ display = kept + f"\n… ({len(lines) - self._max_display_lines} more lines truncated)"
213
+
214
+ return ToolResult(
215
+ data={"agent": snapshot.to_dict()},
216
+ result_for_model=result,
217
+ result_for_display=display,
218
+ is_error=snapshot.status != "completed",
219
+ )
220
+
221
+
222
+ class AgentStatusTool(Tool):
223
+ name = "AgentStatus"
224
+ description = "Inspect one or more managed sub-agents."
225
+ is_read_only = True
226
+ is_concurrency_safe = True
227
+ input_schema = {
228
+ "type": "object",
229
+ "properties": {
230
+ "agent_id": {
231
+ "type": "string",
232
+ "description": "Optional agent ID. Omit to list all agents.",
233
+ }
234
+ },
235
+ }
236
+
237
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
238
+ manager = context.agent_manager
239
+ if not manager:
240
+ return ToolResult(result_for_model="Error: agent manager unavailable", is_error=True)
241
+ agent_id = tool_input.get("agent_id")
242
+ if agent_id:
243
+ snapshot = manager.get_agent(agent_id)
244
+ if not snapshot:
245
+ return ToolResult(result_for_model=f"Error: unknown agent {agent_id}", is_error=True)
246
+ text = AgentManager.format_snapshot(
247
+ snapshot,
248
+ max_result_chars=manager.max_output_chars,
249
+ )
250
+ return ToolResult(data={"agents": [snapshot.to_dict()]}, result_for_model=text)
251
+ snapshots = manager.list_agents()
252
+ if not snapshots:
253
+ return ToolResult(data={"agents": []}, result_for_model="No managed agents.")
254
+ result_budget = max(1, manager.max_output_chars // len(snapshots))
255
+ body = "\n\n".join(
256
+ AgentManager.format_snapshot(
257
+ snapshot,
258
+ max_result_chars=result_budget,
259
+ )
260
+ for snapshot in snapshots
261
+ )
262
+ return ToolResult(
263
+ data={"agents": [snapshot.to_dict() for snapshot in snapshots]},
264
+ result_for_model=body,
265
+ )
266
+
267
+
268
+ class AgentWaitTool(Tool):
269
+ name = "AgentWait"
270
+ description = "Wait for a managed sub-agent to finish."
271
+ is_read_only = True
272
+ is_concurrency_safe = True
273
+ input_schema = {
274
+ "type": "object",
275
+ "properties": {
276
+ "agent_id": {"type": "string", "description": "Agent ID to wait for."},
277
+ "agent_ids": {
278
+ "type": "array",
279
+ "items": {"type": "string"},
280
+ "description": "Optional list of agent IDs to wait on.",
281
+ },
282
+ "wait_any": {
283
+ "type": "boolean",
284
+ "description": "If true, return when any listed agent completes.",
285
+ "default": True,
286
+ },
287
+ "timeout_seconds": {
288
+ "type": "integer",
289
+ "description": "Optional timeout in seconds.",
290
+ },
291
+ },
292
+ }
293
+
294
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
295
+ manager = context.agent_manager
296
+ if not manager:
297
+ return ToolResult(result_for_model="Error: agent manager unavailable", is_error=True)
298
+ timeout = tool_input.get("timeout_seconds")
299
+ timeout_ms = None if timeout is None else int(timeout) * 1000
300
+ agent_ids = [str(v) for v in tool_input.get("agent_ids", []) if str(v)]
301
+ agent_id = tool_input.get("agent_id")
302
+ if agent_id:
303
+ agent_ids.append(str(agent_id))
304
+ if not agent_ids:
305
+ return ToolResult(result_for_model="Error: agent_id or agent_ids is required.", is_error=True)
306
+ if len(agent_ids) == 1 or not tool_input.get("wait_any", True):
307
+ snapshot = await manager.wait_agent(agent_ids[0], timeout_ms=timeout_ms)
308
+ else:
309
+ snapshot = await manager.wait_any(agent_ids, timeout_ms=timeout_ms)
310
+ if snapshot is None:
311
+ return ToolResult(
312
+ data={"agent_ids": agent_ids, "status": "timeout"},
313
+ result_for_model="status: timeout\nresult:\nAgent is still running.",
314
+ is_error=True,
315
+ )
316
+ text = AgentManager.format_snapshot(
317
+ snapshot,
318
+ max_result_chars=manager.max_output_chars,
319
+ )
320
+ return ToolResult(
321
+ data={"agent": snapshot.to_dict()},
322
+ result_for_model=text,
323
+ is_error=snapshot.status
324
+ not in {"completed", "stopped", "cancelled", "failed"},
325
+ )
326
+
327
+
328
+ class AgentCancelTool(Tool):
329
+ name = "AgentCancel"
330
+ description = "Cancel a running managed sub-agent."
331
+ is_read_only = False
332
+ is_concurrency_safe = True
333
+ input_schema = {
334
+ "type": "object",
335
+ "properties": {
336
+ "agent_id": {"type": "string", "description": "Agent ID to cancel."}
337
+ },
338
+ "required": ["agent_id"],
339
+ }
340
+
341
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
342
+ manager = context.agent_manager
343
+ if not manager:
344
+ return ToolResult(result_for_model="Error: agent manager unavailable", is_error=True)
345
+ cancelled = await manager.cancel_agent(tool_input["agent_id"])
346
+ if not cancelled:
347
+ return ToolResult(
348
+ data={"agent_id": tool_input["agent_id"], "cancelled": False},
349
+ result_for_model="Error: agent is not running or does not exist.",
350
+ is_error=True,
351
+ )
352
+ snapshot = manager.get_agent(tool_input["agent_id"])
353
+ text = (
354
+ AgentManager.format_snapshot(
355
+ snapshot,
356
+ max_result_chars=manager.max_output_chars,
357
+ )
358
+ if snapshot
359
+ else f"Cancelled agent: {tool_input['agent_id']}"
360
+ )
361
+ return ToolResult(
362
+ data={"agent": snapshot.to_dict() if snapshot else None, "cancelled": True},
363
+ result_for_model=text,
364
+ )
365
+
366
+
367
+ class AgentSendInputTool(Tool):
368
+ name = "AgentSendInput"
369
+ description = "Send another prompt to an existing managed sub-agent."
370
+ is_read_only = False
371
+ is_concurrency_safe = True
372
+ input_schema = {
373
+ "type": "object",
374
+ "properties": {
375
+ "agent_id": {"type": "string", "description": "Agent ID to continue."},
376
+ "prompt": {"type": "string", "description": "Additional input for the agent."},
377
+ "interrupt": {
378
+ "type": "boolean",
379
+ "description": "Cancel a currently running agent before sending input.",
380
+ "default": False,
381
+ },
382
+ },
383
+ "required": ["agent_id", "prompt"],
384
+ }
385
+
386
+ async def call(self, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
387
+ manager = context.agent_manager
388
+ if not manager:
389
+ return ToolResult(result_for_model="Error: agent manager unavailable", is_error=True)
390
+ sent = await manager.send_input(
391
+ tool_input["agent_id"],
392
+ tool_input["prompt"],
393
+ interrupt=bool(tool_input.get("interrupt", False)),
394
+ )
395
+ if not sent:
396
+ return ToolResult(
397
+ data={"agent_id": tool_input["agent_id"], "sent": False},
398
+ result_for_model="Error: failed to send input to agent.",
399
+ is_error=True,
400
+ )
401
+ snapshot = manager.get_agent(tool_input["agent_id"])
402
+ text = (
403
+ AgentManager.format_snapshot(
404
+ snapshot,
405
+ max_result_chars=manager.max_output_chars,
406
+ )
407
+ if snapshot
408
+ else f"Sent input to agent: {tool_input['agent_id']}"
409
+ )
410
+ return ToolResult(
411
+ data={"agent": snapshot.to_dict() if snapshot else None, "sent": True},
412
+ result_for_model=text,
413
+ )