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,269 @@
|
|
|
1
|
+
"""AgentTool — spawns sub-agents with tool filtering and progress tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from iac_code.agent.agent_types import filter_tools, get_agent_definition, get_builtin_agents
|
|
10
|
+
from iac_code.i18n import _
|
|
11
|
+
from iac_code.tools.base import Tool, ToolContext, ToolResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AgentProgress:
|
|
16
|
+
"""Tracks sub-agent execution progress."""
|
|
17
|
+
|
|
18
|
+
tool_use_count: int = 0
|
|
19
|
+
token_count: int = 0
|
|
20
|
+
last_activity: str = ""
|
|
21
|
+
summary: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def run_sub_agent(
|
|
25
|
+
*,
|
|
26
|
+
prompt: str,
|
|
27
|
+
agent_type: str = "general-purpose",
|
|
28
|
+
cwd: str | None = None,
|
|
29
|
+
parent_provider_manager: Any = None,
|
|
30
|
+
parent_tool_registry: Any = None,
|
|
31
|
+
parent_system_prompt: str = "",
|
|
32
|
+
event_queue: asyncio.Queue | None = None,
|
|
33
|
+
) -> tuple[str, AgentProgress]:
|
|
34
|
+
"""Run a sub-agent and return (final_text, progress)."""
|
|
35
|
+
from iac_code.agent.agent_loop import AgentLoop
|
|
36
|
+
from iac_code.agent.system_prompt import build_system_prompt
|
|
37
|
+
from iac_code.types.stream_events import TextDeltaEvent, ToolResultEvent, ToolUseEndEvent, ToolUseStartEvent
|
|
38
|
+
|
|
39
|
+
defn = get_agent_definition(agent_type)
|
|
40
|
+
if defn is None:
|
|
41
|
+
raise ValueError(f"Unknown agent type: {agent_type}")
|
|
42
|
+
|
|
43
|
+
sub_registry = filter_tools(parent_tool_registry, defn) if parent_tool_registry else parent_tool_registry
|
|
44
|
+
system_prompt = parent_system_prompt or build_system_prompt(cwd=cwd)
|
|
45
|
+
|
|
46
|
+
sub_loop = AgentLoop(
|
|
47
|
+
provider_manager=parent_provider_manager,
|
|
48
|
+
system_prompt=system_prompt,
|
|
49
|
+
tool_registry=sub_registry or parent_tool_registry,
|
|
50
|
+
max_turns=defn.max_turns,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
progress = AgentProgress(summary=f"Running {agent_type} agent")
|
|
54
|
+
text_chunks: list[str] = []
|
|
55
|
+
# Track tool inputs: tool_use_id -> (name, input)
|
|
56
|
+
pending_tool_inputs: dict[str, tuple[str, dict]] = {}
|
|
57
|
+
|
|
58
|
+
async for event in sub_loop.run_streaming(prompt):
|
|
59
|
+
if isinstance(event, TextDeltaEvent):
|
|
60
|
+
text_chunks.append(event.text)
|
|
61
|
+
elif isinstance(event, ToolUseStartEvent):
|
|
62
|
+
pending_tool_inputs[event.tool_use_id] = (event.name, {})
|
|
63
|
+
elif isinstance(event, ToolUseEndEvent):
|
|
64
|
+
if event.tool_use_id in pending_tool_inputs:
|
|
65
|
+
name = pending_tool_inputs[event.tool_use_id][0]
|
|
66
|
+
pending_tool_inputs[event.tool_use_id] = (name, event.input)
|
|
67
|
+
if event_queue:
|
|
68
|
+
tool_input = event.input
|
|
69
|
+
tool_name = pending_tool_inputs.get(event.tool_use_id, ("", {}))[0]
|
|
70
|
+
await event_queue.put(
|
|
71
|
+
{
|
|
72
|
+
"child_tool_name": tool_name,
|
|
73
|
+
"child_tool_input": tool_input,
|
|
74
|
+
"is_done": False,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
elif isinstance(event, ToolResultEvent):
|
|
78
|
+
progress.tool_use_count += 1
|
|
79
|
+
progress.last_activity = event.tool_name
|
|
80
|
+
tool_input = pending_tool_inputs.pop(event.tool_use_id, ("", {}))[1]
|
|
81
|
+
if event_queue:
|
|
82
|
+
await event_queue.put(
|
|
83
|
+
{
|
|
84
|
+
"child_tool_name": event.tool_name,
|
|
85
|
+
"child_tool_input": tool_input,
|
|
86
|
+
"is_done": True,
|
|
87
|
+
"is_error": event.is_error,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
progress.token_count = sub_loop.context_manager.get_total_tokens()
|
|
92
|
+
final_text = "".join(text_chunks)
|
|
93
|
+
|
|
94
|
+
words = final_text.split()
|
|
95
|
+
if len(words) > 500:
|
|
96
|
+
final_text = " ".join(words[:500]) + "\n\n[... truncated to 500 words]"
|
|
97
|
+
|
|
98
|
+
return final_text, progress
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AgentTool(Tool):
|
|
102
|
+
"""Tool that spawns sub-agents for complex tasks."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
task_manager: Any = None,
|
|
107
|
+
notification_queue: Any = None,
|
|
108
|
+
provider_manager: Any = None,
|
|
109
|
+
tool_registry: Any = None,
|
|
110
|
+
system_prompt: str = "",
|
|
111
|
+
):
|
|
112
|
+
self._task_manager = task_manager
|
|
113
|
+
self._notification_queue = notification_queue
|
|
114
|
+
self._provider_manager = provider_manager
|
|
115
|
+
self._tool_registry = tool_registry
|
|
116
|
+
self._system_prompt = system_prompt
|
|
117
|
+
self._event_queue: asyncio.Queue | None = None # Set by ToolExecutor via ToolCallRequest
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def name(self) -> str:
|
|
121
|
+
return "agent"
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def description(self) -> str:
|
|
125
|
+
agents = get_builtin_agents()
|
|
126
|
+
agent_list = "\n".join(f" - {a.agent_type}: {a.when_to_use}" for a in agents)
|
|
127
|
+
return f"Launch a sub-agent to handle complex tasks.\n\nAvailable agent types:\n{agent_list}"
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def input_schema(self) -> dict[str, Any]:
|
|
131
|
+
agent_types = [a.agent_type for a in get_builtin_agents()]
|
|
132
|
+
return {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"prompt": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "The task for the sub-agent to perform.",
|
|
138
|
+
},
|
|
139
|
+
"description": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "Short (3-5 word) description of the task.",
|
|
142
|
+
},
|
|
143
|
+
"subagent_type": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"enum": agent_types,
|
|
146
|
+
"description": "The type of specialized agent to use.",
|
|
147
|
+
},
|
|
148
|
+
"run_in_background": {
|
|
149
|
+
"type": "boolean",
|
|
150
|
+
"description": "Run agent in background, parent continues.",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
"required": ["prompt", "description"],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
157
|
+
prompt = tool_input["prompt"]
|
|
158
|
+
agent_type = tool_input.get("subagent_type", tool_input.get("agent_type", "general-purpose"))
|
|
159
|
+
run_in_background = tool_input.get("run_in_background", False)
|
|
160
|
+
|
|
161
|
+
defn = get_agent_definition(agent_type)
|
|
162
|
+
if defn is None:
|
|
163
|
+
return ToolResult.error(f"Unknown agent type: '{agent_type}'")
|
|
164
|
+
|
|
165
|
+
if run_in_background and self._task_manager:
|
|
166
|
+
task_id = self._task_manager.register(
|
|
167
|
+
description=tool_input.get("description", "Sub-agent task"),
|
|
168
|
+
agent_type=agent_type,
|
|
169
|
+
)
|
|
170
|
+
asyncio.create_task(self._run_background(task_id, prompt, agent_type, context))
|
|
171
|
+
return ToolResult.success(f"Background agent launched (task_id: {task_id}, type: {agent_type})")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result_text, progress = await run_sub_agent(
|
|
175
|
+
prompt=prompt,
|
|
176
|
+
agent_type=agent_type,
|
|
177
|
+
cwd=context.cwd,
|
|
178
|
+
parent_provider_manager=self._provider_manager,
|
|
179
|
+
parent_tool_registry=self._tool_registry,
|
|
180
|
+
parent_system_prompt=self._system_prompt,
|
|
181
|
+
event_queue=self._event_queue,
|
|
182
|
+
)
|
|
183
|
+
if self._event_queue:
|
|
184
|
+
await self._event_queue.put(None)
|
|
185
|
+
return ToolResult.success(
|
|
186
|
+
f"{result_text}\n\n[Agent stats: {progress.tool_use_count} tool calls, {progress.token_count} tokens]"
|
|
187
|
+
)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if self._event_queue:
|
|
190
|
+
await self._event_queue.put(None)
|
|
191
|
+
return ToolResult.error(f"Sub-agent failed: {e}")
|
|
192
|
+
|
|
193
|
+
async def _run_background(
|
|
194
|
+
self,
|
|
195
|
+
task_id: str,
|
|
196
|
+
prompt: str,
|
|
197
|
+
agent_type: str,
|
|
198
|
+
context: ToolContext,
|
|
199
|
+
) -> None:
|
|
200
|
+
try:
|
|
201
|
+
result_text, progress = await run_sub_agent(
|
|
202
|
+
prompt=prompt,
|
|
203
|
+
agent_type=agent_type,
|
|
204
|
+
cwd=context.cwd,
|
|
205
|
+
parent_provider_manager=self._provider_manager,
|
|
206
|
+
parent_tool_registry=self._tool_registry,
|
|
207
|
+
parent_system_prompt=self._system_prompt,
|
|
208
|
+
)
|
|
209
|
+
self._task_manager.complete(task_id, result=result_text)
|
|
210
|
+
self._task_manager.update_progress(
|
|
211
|
+
task_id,
|
|
212
|
+
tool_use_count=progress.tool_use_count,
|
|
213
|
+
token_count=progress.token_count,
|
|
214
|
+
)
|
|
215
|
+
if self._notification_queue:
|
|
216
|
+
self._notification_queue.enqueue(
|
|
217
|
+
task_id=task_id,
|
|
218
|
+
message=f"Agent completed: {progress.tool_use_count} tool calls",
|
|
219
|
+
)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self._task_manager.fail(task_id, error=str(e))
|
|
222
|
+
if self._notification_queue:
|
|
223
|
+
self._notification_queue.enqueue(
|
|
224
|
+
task_id=task_id,
|
|
225
|
+
message=f"Agent failed: {e}",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def is_read_only(self, input: dict | None = None) -> bool:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool:
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
def render_tool_use_message(self, input: dict, *, verbose: bool = False) -> str | None:
|
|
235
|
+
return input.get("description", "")
|
|
236
|
+
|
|
237
|
+
def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None:
|
|
238
|
+
if is_error:
|
|
239
|
+
return f"Agent error: {output[:200]}"
|
|
240
|
+
if verbose:
|
|
241
|
+
return output
|
|
242
|
+
# Extract stats from the end of the output
|
|
243
|
+
import re
|
|
244
|
+
|
|
245
|
+
match = re.search(r"\[Agent stats: (\d+) tool calls, (\d+) tokens\]", output)
|
|
246
|
+
if match:
|
|
247
|
+
tool_count = match.group(1)
|
|
248
|
+
token_count = int(match.group(2))
|
|
249
|
+
token_display = f"{token_count / 1000:.1f}k" if token_count >= 1000 else str(token_count)
|
|
250
|
+
return _("Done ({tool_count} tool uses · {token_display} tokens)").format(
|
|
251
|
+
tool_count=tool_count,
|
|
252
|
+
token_display=token_display,
|
|
253
|
+
)
|
|
254
|
+
return None # Let renderer handle as default
|
|
255
|
+
|
|
256
|
+
def user_facing_name(self, input: dict | None = None) -> str:
|
|
257
|
+
if input:
|
|
258
|
+
agent_type = input.get("subagent_type", input.get("agent_type", "general-purpose"))
|
|
259
|
+
return {
|
|
260
|
+
"explore": _("Explore"),
|
|
261
|
+
"plan": _("Plan"),
|
|
262
|
+
"general-purpose": _("Agent"),
|
|
263
|
+
}.get(agent_type, _("Agent"))
|
|
264
|
+
return _("Agent")
|
|
265
|
+
|
|
266
|
+
def get_activity_description(self, input: dict | None = None) -> str | None:
|
|
267
|
+
if input is None:
|
|
268
|
+
return None
|
|
269
|
+
return f"Running agent: {input.get('description', 'sub-agent')}"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Agent definition model, built-in agent types, and tool filtering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from iac_code.tools.base import ToolRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AgentDefinition:
|
|
14
|
+
"""Structured definition of an agent type."""
|
|
15
|
+
|
|
16
|
+
agent_type: str
|
|
17
|
+
when_to_use: str
|
|
18
|
+
tools: list[str] | None = field(default_factory=lambda: ["*"])
|
|
19
|
+
disallowed_tools: list[str] | None = field(default_factory=list)
|
|
20
|
+
max_turns: int = 50
|
|
21
|
+
model: str = "inherit"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def allows_all_tools(self) -> bool:
|
|
25
|
+
return self.tools is not None and "*" in self.tools
|
|
26
|
+
|
|
27
|
+
def is_tool_allowed(self, tool_name: str) -> bool:
|
|
28
|
+
if self.disallowed_tools and tool_name in self.disallowed_tools:
|
|
29
|
+
return False
|
|
30
|
+
if self.allows_all_tools:
|
|
31
|
+
return True
|
|
32
|
+
if self.tools:
|
|
33
|
+
return tool_name in self.tools
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def filter_tools(registry: "ToolRegistry", agent_def: AgentDefinition) -> "ToolRegistry":
|
|
38
|
+
"""Create a new ToolRegistry containing only tools allowed by the agent definition."""
|
|
39
|
+
from iac_code.tools.base import ToolRegistry
|
|
40
|
+
|
|
41
|
+
filtered = ToolRegistry()
|
|
42
|
+
for tool in registry.list_tools():
|
|
43
|
+
if agent_def.is_tool_allowed(tool.name):
|
|
44
|
+
filtered.register(tool)
|
|
45
|
+
return filtered
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_builtin_agents() -> list[AgentDefinition]:
|
|
49
|
+
return [
|
|
50
|
+
AgentDefinition(
|
|
51
|
+
agent_type="general-purpose",
|
|
52
|
+
when_to_use=(
|
|
53
|
+
"Use for complex, multi-step tasks that require research, "
|
|
54
|
+
"code changes, or coordinating multiple operations."
|
|
55
|
+
),
|
|
56
|
+
tools=["*"],
|
|
57
|
+
disallowed_tools=["agent"],
|
|
58
|
+
max_turns=100,
|
|
59
|
+
),
|
|
60
|
+
AgentDefinition(
|
|
61
|
+
agent_type="explore",
|
|
62
|
+
when_to_use=(
|
|
63
|
+
"Use to quickly find files, search code, or answer questions "
|
|
64
|
+
"about the codebase. Read-only — cannot modify files."
|
|
65
|
+
),
|
|
66
|
+
tools=["read_file", "glob", "grep", "list_files", "bash"],
|
|
67
|
+
disallowed_tools=["write_file", "edit_file", "agent"],
|
|
68
|
+
max_turns=30,
|
|
69
|
+
),
|
|
70
|
+
AgentDefinition(
|
|
71
|
+
agent_type="plan",
|
|
72
|
+
when_to_use=(
|
|
73
|
+
"Use to plan implementation strategy, review architecture, "
|
|
74
|
+
"or design solutions. Read-only, no execution."
|
|
75
|
+
),
|
|
76
|
+
tools=["read_file", "glob", "grep", "list_files"],
|
|
77
|
+
disallowed_tools=["bash", "write_file", "edit_file", "agent"],
|
|
78
|
+
max_turns=20,
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_agent_definition(agent_type: str) -> AgentDefinition | None:
|
|
84
|
+
for defn in get_builtin_agents():
|
|
85
|
+
if defn.agent_type == agent_type:
|
|
86
|
+
return defn
|
|
87
|
+
return None
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Core message types for the agent system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextBlock(BaseModel):
|
|
12
|
+
"""A text content block."""
|
|
13
|
+
|
|
14
|
+
type: Literal["text"] = "text"
|
|
15
|
+
text: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToolUseBlock(BaseModel):
|
|
19
|
+
"""A tool use request block from the assistant."""
|
|
20
|
+
|
|
21
|
+
type: Literal["tool_use"] = "tool_use"
|
|
22
|
+
id: str = Field(default_factory=lambda: f"toolu_{uuid.uuid4().hex[:24]}")
|
|
23
|
+
name: str
|
|
24
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ToolResultBlock(BaseModel):
|
|
28
|
+
"""A tool result block sent back to the assistant."""
|
|
29
|
+
|
|
30
|
+
type: Literal["tool_result"] = "tool_result"
|
|
31
|
+
tool_use_id: str
|
|
32
|
+
content: str
|
|
33
|
+
is_error: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ThinkingBlock(BaseModel):
|
|
37
|
+
"""An assistant reasoning/thinking block.
|
|
38
|
+
|
|
39
|
+
Used to round-trip reasoning content for models that require it
|
|
40
|
+
(DeepSeek V4 thinking mode, Qwen thinking mode, Anthropic extended thinking).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
type: Literal["thinking"] = "thinking"
|
|
44
|
+
thinking: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Union type for all content blocks
|
|
48
|
+
ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Message(BaseModel):
|
|
52
|
+
"""A single message in the conversation."""
|
|
53
|
+
|
|
54
|
+
role: Literal["user", "assistant"]
|
|
55
|
+
content: str | list[ContentBlock]
|
|
56
|
+
token_count: int = 0
|
|
57
|
+
elapsed_seconds: float = 0.0
|
|
58
|
+
|
|
59
|
+
def get_text(self) -> str:
|
|
60
|
+
"""Extract text content from the message."""
|
|
61
|
+
if isinstance(self.content, str):
|
|
62
|
+
return self.content
|
|
63
|
+
return "\n".join(block.text for block in self.content if isinstance(block, TextBlock))
|
|
64
|
+
|
|
65
|
+
def get_tool_use_blocks(self) -> list[ToolUseBlock]:
|
|
66
|
+
"""Extract tool use blocks from the message."""
|
|
67
|
+
if isinstance(self.content, str):
|
|
68
|
+
return []
|
|
69
|
+
return [block for block in self.content if isinstance(block, ToolUseBlock)]
|
|
70
|
+
|
|
71
|
+
def has_tool_use(self) -> bool:
|
|
72
|
+
"""Check if this message contains tool use blocks."""
|
|
73
|
+
return len(self.get_tool_use_blocks()) > 0
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict:
|
|
76
|
+
"""Serialize to a JSON-compatible dict for JSONL persistence."""
|
|
77
|
+
return self.model_dump()
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: dict) -> "Message":
|
|
81
|
+
"""Deserialize from a dict."""
|
|
82
|
+
return cls.model_validate(data)
|
|
83
|
+
|
|
84
|
+
def to_api_format(self) -> dict:
|
|
85
|
+
"""Convert to API-compatible format for litellm."""
|
|
86
|
+
if isinstance(self.content, str):
|
|
87
|
+
return {"role": self.role, "content": self.content}
|
|
88
|
+
|
|
89
|
+
content_list = []
|
|
90
|
+
for block in self.content:
|
|
91
|
+
if isinstance(block, TextBlock):
|
|
92
|
+
content_list.append({"type": "text", "text": block.text})
|
|
93
|
+
elif isinstance(block, ToolUseBlock):
|
|
94
|
+
content_list.append(
|
|
95
|
+
{
|
|
96
|
+
"type": "tool_use",
|
|
97
|
+
"id": block.id,
|
|
98
|
+
"name": block.name,
|
|
99
|
+
"input": block.input,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
elif isinstance(block, ToolResultBlock):
|
|
103
|
+
content_list.append(
|
|
104
|
+
{
|
|
105
|
+
"type": "tool_result",
|
|
106
|
+
"tool_use_id": block.tool_use_id,
|
|
107
|
+
"content": block.content,
|
|
108
|
+
"is_error": block.is_error,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
elif isinstance(block, ThinkingBlock):
|
|
112
|
+
content_list.append({"type": "thinking", "thinking": block.thinking})
|
|
113
|
+
return {"role": self.role, "content": content_list}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Conversation(BaseModel):
|
|
117
|
+
"""Manages the conversation message history."""
|
|
118
|
+
|
|
119
|
+
messages: list[Message] = Field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
def add_user_message(self, content: str) -> Message:
|
|
122
|
+
"""Add a user message to the conversation."""
|
|
123
|
+
msg = Message(role="user", content=content)
|
|
124
|
+
self.messages.append(msg)
|
|
125
|
+
return msg
|
|
126
|
+
|
|
127
|
+
def add_assistant_message(self, content: str | list[ContentBlock]) -> Message:
|
|
128
|
+
"""Add an assistant message to the conversation."""
|
|
129
|
+
msg = Message(role="assistant", content=content)
|
|
130
|
+
self.messages.append(msg)
|
|
131
|
+
return msg
|
|
132
|
+
|
|
133
|
+
def add_tool_results(self, tool_results: list[ToolResultBlock]) -> Message:
|
|
134
|
+
"""Add tool results as a user message."""
|
|
135
|
+
msg = Message(role="user", content=list(tool_results))
|
|
136
|
+
self.messages.append(msg)
|
|
137
|
+
return msg
|
|
138
|
+
|
|
139
|
+
def to_api_format(self) -> list[dict]:
|
|
140
|
+
"""Convert the entire conversation to API format."""
|
|
141
|
+
return [msg.to_api_format() for msg in self.messages]
|
|
142
|
+
|
|
143
|
+
def to_api_messages(self) -> list[dict]:
|
|
144
|
+
"""Alias for to_api_format() for clarity in context management."""
|
|
145
|
+
return self.to_api_format()
|
|
146
|
+
|
|
147
|
+
def get_total_tokens(self) -> int:
|
|
148
|
+
"""Sum token counts across all messages."""
|
|
149
|
+
return sum(msg.token_count for msg in self.messages)
|
|
150
|
+
|
|
151
|
+
def replace_messages(self, messages: list[Message]) -> None:
|
|
152
|
+
"""Replace all messages (used after compaction)."""
|
|
153
|
+
self.messages = messages
|