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,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
|
+
)
|