iac-code 0.1.0__py3-none-any.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.
- iac_code/__init__.py +2 -0
- iac_code/acp/__init__.py +97 -0
- iac_code/acp/convert.py +423 -0
- iac_code/acp/http_sse.py +448 -0
- iac_code/acp/mcp.py +54 -0
- iac_code/acp/metrics.py +71 -0
- iac_code/acp/server.py +662 -0
- iac_code/acp/session.py +446 -0
- iac_code/acp/slash_registry.py +125 -0
- iac_code/acp/state.py +99 -0
- iac_code/acp/tools.py +112 -0
- iac_code/acp/types.py +13 -0
- iac_code/acp/version.py +26 -0
- iac_code/agent/__init__.py +19 -0
- iac_code/agent/agent_loop.py +640 -0
- iac_code/agent/agent_tool.py +269 -0
- iac_code/agent/agent_types.py +87 -0
- iac_code/agent/message.py +153 -0
- iac_code/agent/system_prompt.py +313 -0
- iac_code/cli/__init__.py +3 -0
- iac_code/cli/headless.py +114 -0
- iac_code/cli/main.py +246 -0
- iac_code/cli/output_formats.py +125 -0
- iac_code/commands/__init__.py +93 -0
- iac_code/commands/auth.py +1055 -0
- iac_code/commands/clear.py +34 -0
- iac_code/commands/compact.py +43 -0
- iac_code/commands/debug.py +45 -0
- iac_code/commands/effort.py +116 -0
- iac_code/commands/exit.py +10 -0
- iac_code/commands/help.py +49 -0
- iac_code/commands/model.py +130 -0
- iac_code/commands/registry.py +245 -0
- iac_code/commands/resume.py +49 -0
- iac_code/commands/tasks.py +41 -0
- iac_code/config.py +304 -0
- iac_code/i18n/__init__.py +141 -0
- iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
- iac_code/memory/__init__.py +1 -0
- iac_code/memory/memory_manager.py +92 -0
- iac_code/memory/memory_tools.py +88 -0
- iac_code/providers/__init__.py +1 -0
- iac_code/providers/anthropic_provider.py +284 -0
- iac_code/providers/base.py +128 -0
- iac_code/providers/dashscope_provider.py +47 -0
- iac_code/providers/deepseek_provider.py +36 -0
- iac_code/providers/manager.py +399 -0
- iac_code/providers/openai_provider.py +344 -0
- iac_code/providers/retry.py +58 -0
- iac_code/providers/stream_watchdog.py +47 -0
- iac_code/providers/thinking.py +164 -0
- iac_code/services/__init__.py +1 -0
- iac_code/services/agent_factory.py +127 -0
- iac_code/services/cloud_credentials.py +22 -0
- iac_code/services/context_manager.py +221 -0
- iac_code/services/providers/__init__.py +1 -0
- iac_code/services/providers/aliyun.py +232 -0
- iac_code/services/session_index.py +281 -0
- iac_code/services/session_storage.py +245 -0
- iac_code/services/telemetry/__init__.py +66 -0
- iac_code/services/telemetry/attributes.py +84 -0
- iac_code/services/telemetry/client.py +330 -0
- iac_code/services/telemetry/config.py +76 -0
- iac_code/services/telemetry/constants.py +75 -0
- iac_code/services/telemetry/content_serializer.py +124 -0
- iac_code/services/telemetry/events.py +42 -0
- iac_code/services/telemetry/fallback.py +59 -0
- iac_code/services/telemetry/identity.py +73 -0
- iac_code/services/telemetry/metrics.py +62 -0
- iac_code/services/telemetry/names.py +199 -0
- iac_code/services/telemetry/sanitize.py +88 -0
- iac_code/services/telemetry/sink.py +67 -0
- iac_code/services/telemetry/tracing.py +38 -0
- iac_code/services/telemetry/types.py +13 -0
- iac_code/services/token_budget.py +54 -0
- iac_code/services/token_counter.py +76 -0
- iac_code/skills/__init__.py +1 -0
- iac_code/skills/bundled/__init__.py +94 -0
- iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
- iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
- iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
- iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
- iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
- iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
- iac_code/skills/bundled/simplify.py +28 -0
- iac_code/skills/discovery.py +136 -0
- iac_code/skills/frontmatter.py +119 -0
- iac_code/skills/listing.py +92 -0
- iac_code/skills/loader.py +42 -0
- iac_code/skills/processor.py +81 -0
- iac_code/skills/renderer.py +157 -0
- iac_code/skills/skill_definition.py +82 -0
- iac_code/skills/skill_tool.py +261 -0
- iac_code/state/__init__.py +5 -0
- iac_code/state/app_state.py +122 -0
- iac_code/tasks/__init__.py +1 -0
- iac_code/tasks/notification_queue.py +28 -0
- iac_code/tasks/task_state.py +66 -0
- iac_code/tasks/task_tools.py +114 -0
- iac_code/tools/__init__.py +8 -0
- iac_code/tools/base.py +226 -0
- iac_code/tools/bash.py +133 -0
- iac_code/tools/cloud/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
- iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
- iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
- iac_code/tools/cloud/aliyun/ros_client.py +56 -0
- iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
- iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
- iac_code/tools/cloud/base_api.py +162 -0
- iac_code/tools/cloud/base_stack.py +242 -0
- iac_code/tools/cloud/registry.py +20 -0
- iac_code/tools/cloud/types.py +105 -0
- iac_code/tools/edit_file.py +121 -0
- iac_code/tools/glob.py +103 -0
- iac_code/tools/grep.py +254 -0
- iac_code/tools/list_files.py +104 -0
- iac_code/tools/read_file.py +127 -0
- iac_code/tools/result_storage.py +39 -0
- iac_code/tools/tool_executor.py +165 -0
- iac_code/tools/web_fetch.py +177 -0
- iac_code/tools/write_file.py +88 -0
- iac_code/types/__init__.py +40 -0
- iac_code/types/permissions.py +26 -0
- iac_code/types/skill_source.py +11 -0
- iac_code/types/stream_events.py +227 -0
- iac_code/ui/__init__.py +5 -0
- iac_code/ui/banner.py +110 -0
- iac_code/ui/components/__init__.py +0 -0
- iac_code/ui/components/dialog.py +142 -0
- iac_code/ui/components/divider.py +20 -0
- iac_code/ui/components/fuzzy_picker.py +308 -0
- iac_code/ui/components/progress_bar.py +54 -0
- iac_code/ui/components/search_box.py +165 -0
- iac_code/ui/components/select.py +319 -0
- iac_code/ui/components/status_icon.py +42 -0
- iac_code/ui/components/tabs.py +128 -0
- iac_code/ui/core/__init__.py +0 -0
- iac_code/ui/core/in_place_render.py +129 -0
- iac_code/ui/core/input_history.py +118 -0
- iac_code/ui/core/key_event.py +41 -0
- iac_code/ui/core/prompt_input.py +507 -0
- iac_code/ui/core/raw_input.py +302 -0
- iac_code/ui/core/screen.py +80 -0
- iac_code/ui/dialogs/__init__.py +0 -0
- iac_code/ui/dialogs/global_search.py +178 -0
- iac_code/ui/dialogs/history_search.py +100 -0
- iac_code/ui/dialogs/model_picker.py +280 -0
- iac_code/ui/dialogs/quick_open.py +108 -0
- iac_code/ui/dialogs/resume_picker.py +749 -0
- iac_code/ui/keybindings/__init__.py +0 -0
- iac_code/ui/keybindings/manager.py +124 -0
- iac_code/ui/renderer.py +1535 -0
- iac_code/ui/repl.py +772 -0
- iac_code/ui/spinner.py +112 -0
- iac_code/ui/suggestions/__init__.py +0 -0
- iac_code/ui/suggestions/aggregator.py +171 -0
- iac_code/ui/suggestions/command_provider.py +43 -0
- iac_code/ui/suggestions/directory_provider.py +95 -0
- iac_code/ui/suggestions/file_provider.py +121 -0
- iac_code/ui/suggestions/shell_history_provider.py +108 -0
- iac_code/ui/suggestions/token_extractor.py +77 -0
- iac_code/ui/suggestions/types.py +45 -0
- iac_code/ui/transcript_view.py +199 -0
- iac_code/utils/__init__.py +0 -0
- iac_code/utils/background_housekeeping.py +53 -0
- iac_code/utils/cleanup.py +68 -0
- iac_code/utils/json_utils.py +60 -0
- iac_code/utils/log.py +150 -0
- iac_code/utils/project_paths.py +74 -0
- iac_code/utils/tool_input_parser.py +62 -0
- iac_code-0.1.0.dist-info/LICENSE +201 -0
- iac_code-0.1.0.dist-info/METADATA +64 -0
- iac_code-0.1.0.dist-info/RECORD +184 -0
- iac_code-0.1.0.dist-info/WHEEL +5 -0
- iac_code-0.1.0.dist-info/entry_points.txt +2 -0
- iac_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Background task state management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskStatus(str, Enum):
|
|
12
|
+
RUNNING = "running"
|
|
13
|
+
COMPLETED = "completed"
|
|
14
|
+
FAILED = "failed"
|
|
15
|
+
STOPPED = "stopped"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TaskInfo:
|
|
20
|
+
id: str
|
|
21
|
+
description: str
|
|
22
|
+
agent_type: str = "general-purpose"
|
|
23
|
+
status: TaskStatus = TaskStatus.RUNNING
|
|
24
|
+
result: str | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
tool_use_count: int = 0
|
|
27
|
+
token_count: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskManager:
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._tasks: OrderedDict[str, TaskInfo] = OrderedDict()
|
|
33
|
+
|
|
34
|
+
def register(self, description: str, agent_type: str = "general-purpose") -> str:
|
|
35
|
+
task_id = str(uuid.uuid4())[:8]
|
|
36
|
+
self._tasks[task_id] = TaskInfo(id=task_id, description=description, agent_type=agent_type)
|
|
37
|
+
return task_id
|
|
38
|
+
|
|
39
|
+
def get(self, task_id: str) -> TaskInfo | None:
|
|
40
|
+
return self._tasks.get(task_id)
|
|
41
|
+
|
|
42
|
+
def complete(self, task_id: str, result: str) -> None:
|
|
43
|
+
task = self._tasks.get(task_id)
|
|
44
|
+
if task:
|
|
45
|
+
task.status = TaskStatus.COMPLETED
|
|
46
|
+
task.result = result
|
|
47
|
+
|
|
48
|
+
def fail(self, task_id: str, error: str) -> None:
|
|
49
|
+
task = self._tasks.get(task_id)
|
|
50
|
+
if task:
|
|
51
|
+
task.status = TaskStatus.FAILED
|
|
52
|
+
task.error = error
|
|
53
|
+
|
|
54
|
+
def stop(self, task_id: str) -> None:
|
|
55
|
+
task = self._tasks.get(task_id)
|
|
56
|
+
if task:
|
|
57
|
+
task.status = TaskStatus.STOPPED
|
|
58
|
+
|
|
59
|
+
def update_progress(self, task_id: str, tool_use_count: int = 0, token_count: int = 0) -> None:
|
|
60
|
+
task = self._tasks.get(task_id)
|
|
61
|
+
if task:
|
|
62
|
+
task.tool_use_count = tool_use_count
|
|
63
|
+
task.token_count = token_count
|
|
64
|
+
|
|
65
|
+
def list_all(self) -> list[TaskInfo]:
|
|
66
|
+
return list(self._tasks.values())
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Model-side task tools for managing background agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from iac_code.tasks.task_state import TaskManager
|
|
8
|
+
from iac_code.tools.base import Tool, ToolContext, ToolResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskListTool(Tool):
|
|
12
|
+
def __init__(self, task_manager: TaskManager):
|
|
13
|
+
self._manager = task_manager
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
return "task_list"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def description(self) -> str:
|
|
21
|
+
return "List all background tasks with their status."
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def input_schema(self) -> dict[str, Any]:
|
|
25
|
+
return {"type": "object", "properties": {}}
|
|
26
|
+
|
|
27
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
28
|
+
tasks = self._manager.list_all()
|
|
29
|
+
if not tasks:
|
|
30
|
+
return ToolResult.success("No background tasks.")
|
|
31
|
+
lines = []
|
|
32
|
+
for t in tasks:
|
|
33
|
+
lines.append(f"- [{t.id}] {t.status.value} | [{t.agent_type}] {t.description}")
|
|
34
|
+
if t.result:
|
|
35
|
+
lines.append(f" Result: {t.result[:200]}")
|
|
36
|
+
if t.error:
|
|
37
|
+
lines.append(f" Error: {t.error[:200]}")
|
|
38
|
+
return ToolResult.success("\n".join(lines))
|
|
39
|
+
|
|
40
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskGetTool(Tool):
|
|
45
|
+
def __init__(self, task_manager: TaskManager):
|
|
46
|
+
self._manager = task_manager
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def name(self) -> str:
|
|
50
|
+
return "task_get"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def description(self) -> str:
|
|
54
|
+
return "Get details of a specific background task by ID."
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def input_schema(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {"task_id": {"type": "string"}},
|
|
61
|
+
"required": ["task_id"],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
65
|
+
task = self._manager.get(tool_input["task_id"])
|
|
66
|
+
if not task:
|
|
67
|
+
return ToolResult.error(f"Task '{tool_input['task_id']}' not found.")
|
|
68
|
+
lines = [
|
|
69
|
+
f"ID: {task.id}",
|
|
70
|
+
f"Description: {task.description}",
|
|
71
|
+
f"Status: {task.status.value}",
|
|
72
|
+
f"Agent type: {task.agent_type}",
|
|
73
|
+
f"Tool uses: {task.tool_use_count}",
|
|
74
|
+
f"Tokens: {task.token_count}",
|
|
75
|
+
]
|
|
76
|
+
if task.result:
|
|
77
|
+
lines.append(f"Result: {task.result}")
|
|
78
|
+
if task.error:
|
|
79
|
+
lines.append(f"Error: {task.error}")
|
|
80
|
+
return ToolResult.success("\n".join(lines))
|
|
81
|
+
|
|
82
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TaskStopTool(Tool):
|
|
87
|
+
def __init__(self, task_manager: TaskManager):
|
|
88
|
+
self._manager = task_manager
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def name(self) -> str:
|
|
92
|
+
return "task_stop"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def description(self) -> str:
|
|
96
|
+
return "Stop a running background task."
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def input_schema(self) -> dict[str, Any]:
|
|
100
|
+
return {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {"task_id": {"type": "string"}},
|
|
103
|
+
"required": ["task_id"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
107
|
+
task = self._manager.get(tool_input["task_id"])
|
|
108
|
+
if not task:
|
|
109
|
+
return ToolResult.error(f"Task '{tool_input['task_id']}' not found.")
|
|
110
|
+
self._manager.stop(tool_input["task_id"])
|
|
111
|
+
return ToolResult.success(f"Task '{tool_input['task_id']}' stopped.")
|
|
112
|
+
|
|
113
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
114
|
+
return False
|
iac_code/tools/base.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Base classes for the tool system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from iac_code.types.permissions import PermissionResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolContext:
|
|
18
|
+
"""Execution context passed to tools."""
|
|
19
|
+
|
|
20
|
+
cwd: str = field(default_factory=os.getcwd)
|
|
21
|
+
event_queue: asyncio.Queue | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ToolResult:
|
|
26
|
+
"""Result returned from tool execution.
|
|
27
|
+
|
|
28
|
+
Extended to support context modification:
|
|
29
|
+
- new_messages: Additional messages to inject into conversation context.
|
|
30
|
+
- context_modifier: Callback to modify the execution context
|
|
31
|
+
(e.g., add tool permissions, override model, override effort).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
content: str
|
|
35
|
+
is_error: bool = False
|
|
36
|
+
new_messages: list[dict[str, Any]] = field(default_factory=list)
|
|
37
|
+
context_modifier: Callable[[dict], dict] | None = None
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def error(message: str) -> ToolResult:
|
|
41
|
+
"""Create an error result."""
|
|
42
|
+
return ToolResult(content=message, is_error=True)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def success(content: str) -> ToolResult:
|
|
46
|
+
"""Create a success result."""
|
|
47
|
+
return ToolResult(content=content, is_error=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Tool(ABC):
|
|
51
|
+
"""Abstract base class for all tools."""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
"""The unique name of the tool."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def description(self) -> str:
|
|
62
|
+
"""A description of what the tool does."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def input_schema(self) -> dict[str, Any]:
|
|
68
|
+
"""JSON Schema defining the tool's input parameters."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
73
|
+
"""Execute the tool with the given input.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
tool_input: The input parameters as defined by input_schema.
|
|
77
|
+
context: The execution context.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The result of the tool execution.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
def normalize_input(self, tool_input: dict[str, Any]) -> None:
|
|
85
|
+
"""Normalize tool input in-place before validation.
|
|
86
|
+
|
|
87
|
+
Override in subclasses to handle parameter aliases.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def validate_input(self, tool_input: dict[str, Any]) -> tuple[bool, str]:
|
|
91
|
+
"""Validate tool_input against input_schema using JSON Schema.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(True, "") if valid, (False, error_message) if invalid.
|
|
95
|
+
"""
|
|
96
|
+
self.normalize_input(tool_input)
|
|
97
|
+
try:
|
|
98
|
+
import jsonschema
|
|
99
|
+
|
|
100
|
+
jsonschema.validate(instance=tool_input, schema=self.input_schema)
|
|
101
|
+
return True, ""
|
|
102
|
+
except jsonschema.ValidationError as e:
|
|
103
|
+
return False, str(e.message)
|
|
104
|
+
|
|
105
|
+
def to_api_format(self) -> dict[str, Any]:
|
|
106
|
+
"""Convert tool definition to LLM API format (legacy OpenAI format)."""
|
|
107
|
+
return {
|
|
108
|
+
"type": "function",
|
|
109
|
+
"function": {
|
|
110
|
+
"name": self.name,
|
|
111
|
+
"description": self.description,
|
|
112
|
+
"parameters": self.input_schema,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# --- UI rendering methods ---
|
|
117
|
+
def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
|
|
118
|
+
"""Return detail text shown in parentheses after the tool name.
|
|
119
|
+
|
|
120
|
+
Displayed as ``● ToolName(detail)`` in the UI. Use this to show
|
|
121
|
+
the key parameters of the tool invocation, e.g. file path, search
|
|
122
|
+
pattern, or URL.
|
|
123
|
+
"""
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
|
|
127
|
+
"""Return result summary shown after ``⎿`` prefix.
|
|
128
|
+
|
|
129
|
+
In non-verbose mode, return a short one-line summary (e.g.
|
|
130
|
+
"Found 5 files"). In verbose mode, include more detail.
|
|
131
|
+
"""
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def render_tool_use_error_message(self, error: str) -> str | None:
|
|
135
|
+
"""Return error text for display."""
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# --- Display info ---
|
|
139
|
+
def user_facing_name(self, input: dict | None = None) -> str:
|
|
140
|
+
"""Short, human-readable tool name shown as the bold label in the UI.
|
|
141
|
+
|
|
142
|
+
Should be a concise noun like "Read", "Search", or "Bash".
|
|
143
|
+
Do NOT include parameter details here — use
|
|
144
|
+
``render_tool_use_message`` for that. This name is also used in
|
|
145
|
+
permission prompts (e.g. "Allow Read?").
|
|
146
|
+
"""
|
|
147
|
+
return self.name
|
|
148
|
+
|
|
149
|
+
def get_activity_description(self, input: dict | None = None) -> str | None:
|
|
150
|
+
"""Text shown in the spinner while the tool is executing."""
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def get_tool_use_summary(self, input: dict | None = None) -> str | None:
|
|
154
|
+
"""Optional one-line summary of the tool invocation."""
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# --- Permission methods ---
|
|
158
|
+
@property
|
|
159
|
+
def timeout(self) -> float | None:
|
|
160
|
+
"""Per-tool timeout in seconds. None means use the global default."""
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
164
|
+
"""Whether the tool only reads and never modifies state."""
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
|
|
168
|
+
"""Whether this tool can safely run concurrently with other tools.
|
|
169
|
+
By default, read-only tools are concurrency safe.
|
|
170
|
+
"""
|
|
171
|
+
return self.is_read_only(tool_input)
|
|
172
|
+
|
|
173
|
+
def is_destructive(self, input: dict | None = None) -> bool:
|
|
174
|
+
"""Whether the tool performs destructive operations."""
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
async def check_permissions(self, input: dict, context: dict | None = None) -> "PermissionResult":
|
|
178
|
+
"""Check permissions"""
|
|
179
|
+
from iac_code.types.permissions import PermissionResult
|
|
180
|
+
|
|
181
|
+
if self.is_read_only(input):
|
|
182
|
+
return PermissionResult(behavior="allow")
|
|
183
|
+
return PermissionResult(behavior="ask", message=f"Allow {self.user_facing_name(input)}?")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ToolRegistry:
|
|
187
|
+
"""Registry for managing available tools."""
|
|
188
|
+
|
|
189
|
+
def __init__(self) -> None:
|
|
190
|
+
self._tools: dict[str, Tool] = {}
|
|
191
|
+
|
|
192
|
+
def register(self, tool: Tool) -> None:
|
|
193
|
+
"""Register a tool."""
|
|
194
|
+
self._tools[tool.name] = tool
|
|
195
|
+
|
|
196
|
+
def get(self, name: str) -> Tool | None:
|
|
197
|
+
"""Get a tool by name."""
|
|
198
|
+
return self._tools.get(name)
|
|
199
|
+
|
|
200
|
+
def list_tools(self) -> list[Tool]:
|
|
201
|
+
"""List all registered tools."""
|
|
202
|
+
return list(self._tools.values())
|
|
203
|
+
|
|
204
|
+
def to_api_format(self) -> list[dict[str, Any]]:
|
|
205
|
+
"""Convert all tools to LLM API format (legacy OpenAI format)."""
|
|
206
|
+
return [tool.to_api_format() for tool in self._tools.values()]
|
|
207
|
+
|
|
208
|
+
def register_default_tools(self) -> None:
|
|
209
|
+
"""Register all default built-in tools."""
|
|
210
|
+
from iac_code.tools.bash import BashTool
|
|
211
|
+
from iac_code.tools.edit_file import EditFileTool
|
|
212
|
+
from iac_code.tools.glob import GlobTool
|
|
213
|
+
from iac_code.tools.grep import GrepTool
|
|
214
|
+
from iac_code.tools.list_files import ListFilesTool
|
|
215
|
+
from iac_code.tools.read_file import ReadFileTool
|
|
216
|
+
from iac_code.tools.web_fetch import WebFetchTool
|
|
217
|
+
from iac_code.tools.write_file import WriteFileTool
|
|
218
|
+
|
|
219
|
+
self.register(ReadFileTool())
|
|
220
|
+
self.register(WriteFileTool())
|
|
221
|
+
self.register(EditFileTool())
|
|
222
|
+
self.register(BashTool())
|
|
223
|
+
self.register(ListFilesTool())
|
|
224
|
+
self.register(GlobTool())
|
|
225
|
+
self.register(GrepTool())
|
|
226
|
+
self.register(WebFetchTool())
|
iac_code/tools/bash.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Bash tool - executes shell commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from iac_code.i18n import _
|
|
9
|
+
from iac_code.tools.base import Tool, ToolContext, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BashTool(Tool):
|
|
13
|
+
@property
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
return "bash"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def description(self) -> str:
|
|
19
|
+
return (
|
|
20
|
+
"Execute a shell command in the system's default shell. "
|
|
21
|
+
"Use this for running programs, installing packages, searching code, "
|
|
22
|
+
"running tests, git operations, and other system tasks. "
|
|
23
|
+
"Commands are executed with a timeout of 120 seconds by default."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def input_schema(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"command": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The shell command to execute.",
|
|
34
|
+
},
|
|
35
|
+
"timeout": {
|
|
36
|
+
"type": "integer",
|
|
37
|
+
"description": "Timeout in seconds. Defaults to 120.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
"required": ["command"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
44
|
+
command = tool_input["command"]
|
|
45
|
+
timeout = tool_input.get("timeout", 120)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
process = await asyncio.create_subprocess_shell(
|
|
49
|
+
command,
|
|
50
|
+
stdout=asyncio.subprocess.PIPE,
|
|
51
|
+
stderr=asyncio.subprocess.PIPE,
|
|
52
|
+
cwd=context.cwd,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
57
|
+
except asyncio.TimeoutError:
|
|
58
|
+
process.kill()
|
|
59
|
+
await process.communicate()
|
|
60
|
+
return ToolResult.error(f"Command timed out after {timeout} seconds: {command}")
|
|
61
|
+
|
|
62
|
+
stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
|
|
63
|
+
stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""
|
|
64
|
+
|
|
65
|
+
# Build result
|
|
66
|
+
parts = []
|
|
67
|
+
if stdout_str:
|
|
68
|
+
parts.append(f"STDOUT:\n{stdout_str}")
|
|
69
|
+
if stderr_str:
|
|
70
|
+
parts.append(f"STDERR:\n{stderr_str}")
|
|
71
|
+
parts.append(f"Exit code: {process.returncode}")
|
|
72
|
+
|
|
73
|
+
output = "\n".join(parts)
|
|
74
|
+
|
|
75
|
+
if process.returncode != 0:
|
|
76
|
+
return ToolResult.error(output)
|
|
77
|
+
|
|
78
|
+
return ToolResult.success(output)
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return ToolResult.error(f"Error executing command: {e}")
|
|
82
|
+
|
|
83
|
+
# UI rendering methods
|
|
84
|
+
MAX_COMMAND_DISPLAY_CHARS = 160
|
|
85
|
+
MAX_COMMAND_DISPLAY_LINES = 2
|
|
86
|
+
MAX_OUTPUT_LINES = 20
|
|
87
|
+
|
|
88
|
+
def render_tool_use_message(self, input: dict, *, verbose: bool = False):
|
|
89
|
+
cmd = input.get("command", "")
|
|
90
|
+
if not cmd:
|
|
91
|
+
return None
|
|
92
|
+
if not verbose:
|
|
93
|
+
lines = cmd.split("\n")
|
|
94
|
+
if len(lines) > self.MAX_COMMAND_DISPLAY_LINES:
|
|
95
|
+
cmd = "\n".join(lines[: self.MAX_COMMAND_DISPLAY_LINES])
|
|
96
|
+
if len(cmd) > self.MAX_COMMAND_DISPLAY_CHARS:
|
|
97
|
+
cmd = cmd[: self.MAX_COMMAND_DISPLAY_CHARS]
|
|
98
|
+
return cmd.strip() + "…" if cmd != input.get("command", "") else cmd
|
|
99
|
+
return cmd
|
|
100
|
+
|
|
101
|
+
def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False):
|
|
102
|
+
lines = output.strip().splitlines()
|
|
103
|
+
if not verbose and len(lines) > self.MAX_OUTPUT_LINES:
|
|
104
|
+
return (
|
|
105
|
+
"\n".join(lines[: self.MAX_OUTPUT_LINES])
|
|
106
|
+
+ "\n... "
|
|
107
|
+
+ _("{count} more lines").format(count=len(lines) - self.MAX_OUTPUT_LINES)
|
|
108
|
+
)
|
|
109
|
+
return output.strip()
|
|
110
|
+
|
|
111
|
+
def render_tool_use_error_message(self, error: str):
|
|
112
|
+
return error
|
|
113
|
+
|
|
114
|
+
def user_facing_name(self, input: dict | None = None) -> str:
|
|
115
|
+
return _("Bash")
|
|
116
|
+
|
|
117
|
+
def get_activity_description(self, input: dict | None = None) -> str:
|
|
118
|
+
if input:
|
|
119
|
+
cmd = input.get("command", "")
|
|
120
|
+
short = cmd[:50] + "..." if len(cmd) > 50 else cmd
|
|
121
|
+
return _("Running {cmd}").format(cmd=short)
|
|
122
|
+
return _("Running command...")
|
|
123
|
+
|
|
124
|
+
def get_tool_use_summary(self, input: dict | None = None) -> str | None:
|
|
125
|
+
if input:
|
|
126
|
+
return input.get("command", "")[:80]
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def is_destructive(self, input: dict | None = None) -> bool:
|
|
133
|
+
return False
|
|
File without changes
|
|
File without changes
|