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.
Files changed (184) hide show
  1. iac_code/__init__.py +2 -0
  2. iac_code/acp/__init__.py +97 -0
  3. iac_code/acp/convert.py +423 -0
  4. iac_code/acp/http_sse.py +448 -0
  5. iac_code/acp/mcp.py +54 -0
  6. iac_code/acp/metrics.py +71 -0
  7. iac_code/acp/server.py +662 -0
  8. iac_code/acp/session.py +446 -0
  9. iac_code/acp/slash_registry.py +125 -0
  10. iac_code/acp/state.py +99 -0
  11. iac_code/acp/tools.py +112 -0
  12. iac_code/acp/types.py +13 -0
  13. iac_code/acp/version.py +26 -0
  14. iac_code/agent/__init__.py +19 -0
  15. iac_code/agent/agent_loop.py +640 -0
  16. iac_code/agent/agent_tool.py +269 -0
  17. iac_code/agent/agent_types.py +87 -0
  18. iac_code/agent/message.py +153 -0
  19. iac_code/agent/system_prompt.py +313 -0
  20. iac_code/cli/__init__.py +3 -0
  21. iac_code/cli/headless.py +114 -0
  22. iac_code/cli/main.py +246 -0
  23. iac_code/cli/output_formats.py +125 -0
  24. iac_code/commands/__init__.py +93 -0
  25. iac_code/commands/auth.py +1055 -0
  26. iac_code/commands/clear.py +34 -0
  27. iac_code/commands/compact.py +43 -0
  28. iac_code/commands/debug.py +45 -0
  29. iac_code/commands/effort.py +116 -0
  30. iac_code/commands/exit.py +10 -0
  31. iac_code/commands/help.py +49 -0
  32. iac_code/commands/model.py +130 -0
  33. iac_code/commands/registry.py +245 -0
  34. iac_code/commands/resume.py +49 -0
  35. iac_code/commands/tasks.py +41 -0
  36. iac_code/config.py +304 -0
  37. iac_code/i18n/__init__.py +141 -0
  38. iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
  39. iac_code/memory/__init__.py +1 -0
  40. iac_code/memory/memory_manager.py +92 -0
  41. iac_code/memory/memory_tools.py +88 -0
  42. iac_code/providers/__init__.py +1 -0
  43. iac_code/providers/anthropic_provider.py +284 -0
  44. iac_code/providers/base.py +128 -0
  45. iac_code/providers/dashscope_provider.py +47 -0
  46. iac_code/providers/deepseek_provider.py +36 -0
  47. iac_code/providers/manager.py +399 -0
  48. iac_code/providers/openai_provider.py +344 -0
  49. iac_code/providers/retry.py +58 -0
  50. iac_code/providers/stream_watchdog.py +47 -0
  51. iac_code/providers/thinking.py +164 -0
  52. iac_code/services/__init__.py +1 -0
  53. iac_code/services/agent_factory.py +127 -0
  54. iac_code/services/cloud_credentials.py +22 -0
  55. iac_code/services/context_manager.py +221 -0
  56. iac_code/services/providers/__init__.py +1 -0
  57. iac_code/services/providers/aliyun.py +232 -0
  58. iac_code/services/session_index.py +281 -0
  59. iac_code/services/session_storage.py +245 -0
  60. iac_code/services/telemetry/__init__.py +66 -0
  61. iac_code/services/telemetry/attributes.py +84 -0
  62. iac_code/services/telemetry/client.py +330 -0
  63. iac_code/services/telemetry/config.py +76 -0
  64. iac_code/services/telemetry/constants.py +75 -0
  65. iac_code/services/telemetry/content_serializer.py +124 -0
  66. iac_code/services/telemetry/events.py +42 -0
  67. iac_code/services/telemetry/fallback.py +59 -0
  68. iac_code/services/telemetry/identity.py +73 -0
  69. iac_code/services/telemetry/metrics.py +62 -0
  70. iac_code/services/telemetry/names.py +199 -0
  71. iac_code/services/telemetry/sanitize.py +88 -0
  72. iac_code/services/telemetry/sink.py +67 -0
  73. iac_code/services/telemetry/tracing.py +38 -0
  74. iac_code/services/telemetry/types.py +13 -0
  75. iac_code/services/token_budget.py +54 -0
  76. iac_code/services/token_counter.py +76 -0
  77. iac_code/skills/__init__.py +1 -0
  78. iac_code/skills/bundled/__init__.py +94 -0
  79. iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
  80. iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
  81. iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
  82. iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
  83. iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
  84. iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
  85. iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
  86. iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
  87. iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
  88. iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
  89. iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
  90. iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
  91. iac_code/skills/bundled/simplify.py +28 -0
  92. iac_code/skills/discovery.py +136 -0
  93. iac_code/skills/frontmatter.py +119 -0
  94. iac_code/skills/listing.py +92 -0
  95. iac_code/skills/loader.py +42 -0
  96. iac_code/skills/processor.py +81 -0
  97. iac_code/skills/renderer.py +157 -0
  98. iac_code/skills/skill_definition.py +82 -0
  99. iac_code/skills/skill_tool.py +261 -0
  100. iac_code/state/__init__.py +5 -0
  101. iac_code/state/app_state.py +122 -0
  102. iac_code/tasks/__init__.py +1 -0
  103. iac_code/tasks/notification_queue.py +28 -0
  104. iac_code/tasks/task_state.py +66 -0
  105. iac_code/tasks/task_tools.py +114 -0
  106. iac_code/tools/__init__.py +8 -0
  107. iac_code/tools/base.py +226 -0
  108. iac_code/tools/bash.py +133 -0
  109. iac_code/tools/cloud/__init__.py +0 -0
  110. iac_code/tools/cloud/aliyun/__init__.py +0 -0
  111. iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
  112. iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
  113. iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
  114. iac_code/tools/cloud/aliyun/ros_client.py +56 -0
  115. iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
  116. iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
  117. iac_code/tools/cloud/base_api.py +162 -0
  118. iac_code/tools/cloud/base_stack.py +242 -0
  119. iac_code/tools/cloud/registry.py +20 -0
  120. iac_code/tools/cloud/types.py +105 -0
  121. iac_code/tools/edit_file.py +121 -0
  122. iac_code/tools/glob.py +103 -0
  123. iac_code/tools/grep.py +254 -0
  124. iac_code/tools/list_files.py +104 -0
  125. iac_code/tools/read_file.py +127 -0
  126. iac_code/tools/result_storage.py +39 -0
  127. iac_code/tools/tool_executor.py +165 -0
  128. iac_code/tools/web_fetch.py +177 -0
  129. iac_code/tools/write_file.py +88 -0
  130. iac_code/types/__init__.py +40 -0
  131. iac_code/types/permissions.py +26 -0
  132. iac_code/types/skill_source.py +11 -0
  133. iac_code/types/stream_events.py +227 -0
  134. iac_code/ui/__init__.py +5 -0
  135. iac_code/ui/banner.py +110 -0
  136. iac_code/ui/components/__init__.py +0 -0
  137. iac_code/ui/components/dialog.py +142 -0
  138. iac_code/ui/components/divider.py +20 -0
  139. iac_code/ui/components/fuzzy_picker.py +308 -0
  140. iac_code/ui/components/progress_bar.py +54 -0
  141. iac_code/ui/components/search_box.py +165 -0
  142. iac_code/ui/components/select.py +319 -0
  143. iac_code/ui/components/status_icon.py +42 -0
  144. iac_code/ui/components/tabs.py +128 -0
  145. iac_code/ui/core/__init__.py +0 -0
  146. iac_code/ui/core/in_place_render.py +129 -0
  147. iac_code/ui/core/input_history.py +118 -0
  148. iac_code/ui/core/key_event.py +41 -0
  149. iac_code/ui/core/prompt_input.py +507 -0
  150. iac_code/ui/core/raw_input.py +302 -0
  151. iac_code/ui/core/screen.py +80 -0
  152. iac_code/ui/dialogs/__init__.py +0 -0
  153. iac_code/ui/dialogs/global_search.py +178 -0
  154. iac_code/ui/dialogs/history_search.py +100 -0
  155. iac_code/ui/dialogs/model_picker.py +280 -0
  156. iac_code/ui/dialogs/quick_open.py +108 -0
  157. iac_code/ui/dialogs/resume_picker.py +749 -0
  158. iac_code/ui/keybindings/__init__.py +0 -0
  159. iac_code/ui/keybindings/manager.py +124 -0
  160. iac_code/ui/renderer.py +1535 -0
  161. iac_code/ui/repl.py +772 -0
  162. iac_code/ui/spinner.py +112 -0
  163. iac_code/ui/suggestions/__init__.py +0 -0
  164. iac_code/ui/suggestions/aggregator.py +171 -0
  165. iac_code/ui/suggestions/command_provider.py +43 -0
  166. iac_code/ui/suggestions/directory_provider.py +95 -0
  167. iac_code/ui/suggestions/file_provider.py +121 -0
  168. iac_code/ui/suggestions/shell_history_provider.py +108 -0
  169. iac_code/ui/suggestions/token_extractor.py +77 -0
  170. iac_code/ui/suggestions/types.py +45 -0
  171. iac_code/ui/transcript_view.py +199 -0
  172. iac_code/utils/__init__.py +0 -0
  173. iac_code/utils/background_housekeeping.py +53 -0
  174. iac_code/utils/cleanup.py +68 -0
  175. iac_code/utils/json_utils.py +60 -0
  176. iac_code/utils/log.py +150 -0
  177. iac_code/utils/project_paths.py +74 -0
  178. iac_code/utils/tool_input_parser.py +62 -0
  179. iac_code-0.1.0.dist-info/LICENSE +201 -0
  180. iac_code-0.1.0.dist-info/METADATA +64 -0
  181. iac_code-0.1.0.dist-info/RECORD +184 -0
  182. iac_code-0.1.0.dist-info/WHEEL +5 -0
  183. iac_code-0.1.0.dist-info/entry_points.txt +2 -0
  184. 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