minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
ACP Agent implementation for minion-code.
|
|
5
|
+
|
|
6
|
+
Implements the ACP Agent protocol to allow minion-code to be used
|
|
7
|
+
with ACP-compatible clients.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from acp import Client, text_block
|
|
18
|
+
from acp.helpers import (
|
|
19
|
+
update_agent_message_text,
|
|
20
|
+
update_agent_thought_text,
|
|
21
|
+
)
|
|
22
|
+
from acp.schema import (
|
|
23
|
+
AgentCapabilities,
|
|
24
|
+
AgentMessageChunk,
|
|
25
|
+
AgentThoughtChunk,
|
|
26
|
+
AudioContentBlock,
|
|
27
|
+
AuthenticateResponse,
|
|
28
|
+
ClientCapabilities,
|
|
29
|
+
ContentToolCallContent,
|
|
30
|
+
EmbeddedResourceContentBlock,
|
|
31
|
+
ForkSessionResponse,
|
|
32
|
+
HttpMcpServer,
|
|
33
|
+
ImageContentBlock,
|
|
34
|
+
Implementation,
|
|
35
|
+
InitializeResponse,
|
|
36
|
+
ListSessionsResponse,
|
|
37
|
+
LoadSessionResponse,
|
|
38
|
+
McpServerStdio,
|
|
39
|
+
NewSessionResponse,
|
|
40
|
+
PromptResponse,
|
|
41
|
+
ResourceContentBlock,
|
|
42
|
+
ResumeSessionResponse,
|
|
43
|
+
SetSessionModelResponse,
|
|
44
|
+
SetSessionModeResponse,
|
|
45
|
+
SseMcpServer,
|
|
46
|
+
TextContentBlock,
|
|
47
|
+
ToolCallStart,
|
|
48
|
+
ToolCallProgress,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from ..agents.code_agent import MinionCodeAgent
|
|
52
|
+
from ..agents.hooks import HookConfig, wrap_tools_with_hooks
|
|
53
|
+
from .hooks import create_acp_hooks
|
|
54
|
+
from .permissions import PermissionStore
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
# Protocol version
|
|
59
|
+
PROTOCOL_VERSION = 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MinionACPAgent:
|
|
63
|
+
"""
|
|
64
|
+
ACP Agent implementation wrapping MinionCodeAgent.
|
|
65
|
+
|
|
66
|
+
This class implements the ACP Agent protocol, allowing minion-code
|
|
67
|
+
to communicate with ACP clients over stdio.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
skip_permissions: bool = False,
|
|
73
|
+
config: Optional[Dict] = None,
|
|
74
|
+
cwd: Optional[str] = None,
|
|
75
|
+
model: Optional[str] = None,
|
|
76
|
+
):
|
|
77
|
+
self.client: Optional[Client] = None
|
|
78
|
+
self.sessions: Dict[str, "ACPSession"] = {}
|
|
79
|
+
self._cancel_events: Dict[str, asyncio.Event] = {}
|
|
80
|
+
self.skip_permissions = skip_permissions
|
|
81
|
+
self.config = config or {}
|
|
82
|
+
self.cwd = cwd or os.getcwd()
|
|
83
|
+
self.model = model # LLM model to use
|
|
84
|
+
|
|
85
|
+
def on_connect(self, conn: Client) -> None:
|
|
86
|
+
"""Called when connected to an ACP client."""
|
|
87
|
+
self.client = conn
|
|
88
|
+
logger.info("Connected to ACP client")
|
|
89
|
+
|
|
90
|
+
async def initialize(
|
|
91
|
+
self,
|
|
92
|
+
protocol_version: int,
|
|
93
|
+
client_capabilities: Optional[ClientCapabilities] = None,
|
|
94
|
+
client_info: Optional[Implementation] = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> InitializeResponse:
|
|
97
|
+
"""Initialize the agent and negotiate capabilities."""
|
|
98
|
+
logger.info(f"Initializing with protocol version {protocol_version}")
|
|
99
|
+
|
|
100
|
+
return InitializeResponse(
|
|
101
|
+
protocol_version=min(protocol_version, PROTOCOL_VERSION),
|
|
102
|
+
agent_info=Implementation(
|
|
103
|
+
name="minion-code",
|
|
104
|
+
version="0.1.0",
|
|
105
|
+
),
|
|
106
|
+
agent_capabilities=AgentCapabilities(
|
|
107
|
+
streaming=True,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def new_session(
|
|
112
|
+
self,
|
|
113
|
+
cwd: str,
|
|
114
|
+
mcp_servers: List[HttpMcpServer | SseMcpServer | McpServerStdio],
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
) -> NewSessionResponse:
|
|
117
|
+
"""Create a new session."""
|
|
118
|
+
session_id = str(uuid.uuid4())
|
|
119
|
+
pid = os.getpid()
|
|
120
|
+
session_count = len(self.sessions) + 1
|
|
121
|
+
|
|
122
|
+
# Use CLI-provided cwd as fallback if client doesn't provide one
|
|
123
|
+
if not cwd:
|
|
124
|
+
cwd = self.cwd
|
|
125
|
+
logger.info(
|
|
126
|
+
f"[PID={pid}] Creating session #{session_count}: {session_id} in {cwd}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Create session
|
|
130
|
+
session = ACPSession(
|
|
131
|
+
session_id=session_id,
|
|
132
|
+
cwd=cwd,
|
|
133
|
+
client=self.client,
|
|
134
|
+
mcp_servers=mcp_servers,
|
|
135
|
+
skip_permissions=self.skip_permissions,
|
|
136
|
+
model=self.model,
|
|
137
|
+
)
|
|
138
|
+
await session.initialize()
|
|
139
|
+
|
|
140
|
+
self.sessions[session_id] = session
|
|
141
|
+
self._cancel_events[session_id] = asyncio.Event()
|
|
142
|
+
|
|
143
|
+
return NewSessionResponse(session_id=session_id)
|
|
144
|
+
|
|
145
|
+
async def load_session(
|
|
146
|
+
self,
|
|
147
|
+
cwd: str,
|
|
148
|
+
mcp_servers: List[HttpMcpServer | SseMcpServer | McpServerStdio],
|
|
149
|
+
session_id: str,
|
|
150
|
+
**kwargs: Any,
|
|
151
|
+
) -> Optional[LoadSessionResponse]:
|
|
152
|
+
"""Load an existing session."""
|
|
153
|
+
if session_id in self.sessions:
|
|
154
|
+
return LoadSessionResponse()
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
async def list_sessions(
|
|
158
|
+
self,
|
|
159
|
+
cursor: Optional[str] = None,
|
|
160
|
+
cwd: Optional[str] = None,
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> ListSessionsResponse:
|
|
163
|
+
"""List available sessions."""
|
|
164
|
+
# Simple implementation - just return session IDs
|
|
165
|
+
sessions = []
|
|
166
|
+
for sid in self.sessions.keys():
|
|
167
|
+
sessions.append({"session_id": sid})
|
|
168
|
+
|
|
169
|
+
return ListSessionsResponse(sessions=sessions)
|
|
170
|
+
|
|
171
|
+
async def set_session_mode(
|
|
172
|
+
self,
|
|
173
|
+
mode_id: str,
|
|
174
|
+
session_id: str,
|
|
175
|
+
**kwargs: Any,
|
|
176
|
+
) -> Optional[SetSessionModeResponse]:
|
|
177
|
+
"""Set the session mode (not implemented)."""
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
async def set_session_model(
|
|
181
|
+
self,
|
|
182
|
+
model_id: str,
|
|
183
|
+
session_id: str,
|
|
184
|
+
**kwargs: Any,
|
|
185
|
+
) -> Optional[SetSessionModelResponse]:
|
|
186
|
+
"""Set the session model (not implemented)."""
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
async def authenticate(
|
|
190
|
+
self,
|
|
191
|
+
method_id: str,
|
|
192
|
+
**kwargs: Any,
|
|
193
|
+
) -> Optional[AuthenticateResponse]:
|
|
194
|
+
"""Authenticate (not implemented)."""
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
async def prompt(
|
|
198
|
+
self,
|
|
199
|
+
prompt: List[
|
|
200
|
+
TextContentBlock
|
|
201
|
+
| ImageContentBlock
|
|
202
|
+
| AudioContentBlock
|
|
203
|
+
| ResourceContentBlock
|
|
204
|
+
| EmbeddedResourceContentBlock
|
|
205
|
+
],
|
|
206
|
+
session_id: str,
|
|
207
|
+
**kwargs: Any,
|
|
208
|
+
) -> PromptResponse:
|
|
209
|
+
"""Process a user prompt."""
|
|
210
|
+
pid = os.getpid()
|
|
211
|
+
logger.info(f"[PID={pid}] Processing prompt for session {session_id}")
|
|
212
|
+
logger.info(f"[PID={pid}] Prompt content: {prompt}")
|
|
213
|
+
|
|
214
|
+
session = self.sessions.get(session_id)
|
|
215
|
+
if not session:
|
|
216
|
+
logger.error(f"Session {session_id} not found")
|
|
217
|
+
return PromptResponse(stop_reason="refusal")
|
|
218
|
+
|
|
219
|
+
# Clear cancel event
|
|
220
|
+
cancel_event = self._cancel_events.get(session_id)
|
|
221
|
+
if cancel_event:
|
|
222
|
+
cancel_event.clear()
|
|
223
|
+
|
|
224
|
+
# Extract text from prompt (handle both Pydantic models and dicts)
|
|
225
|
+
text_parts = []
|
|
226
|
+
for block in prompt:
|
|
227
|
+
if isinstance(block, dict):
|
|
228
|
+
# Dict format
|
|
229
|
+
if block.get("type") == "text":
|
|
230
|
+
text_parts.append(block.get("text", ""))
|
|
231
|
+
elif isinstance(block, TextContentBlock):
|
|
232
|
+
text_parts.append(block.text)
|
|
233
|
+
elif hasattr(block, "text"):
|
|
234
|
+
text_parts.append(block.text)
|
|
235
|
+
|
|
236
|
+
user_message = "\n".join(text_parts)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# Run the agent
|
|
240
|
+
stop_reason = await session.run_prompt(
|
|
241
|
+
message=user_message,
|
|
242
|
+
cancel_event=cancel_event,
|
|
243
|
+
)
|
|
244
|
+
return PromptResponse(stop_reason=stop_reason)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Error processing prompt: {e}")
|
|
247
|
+
# Send error message
|
|
248
|
+
if self.client:
|
|
249
|
+
await self.client.session_update(
|
|
250
|
+
session_id=session_id,
|
|
251
|
+
update=update_agent_message_text(f"Error: {str(e)}"),
|
|
252
|
+
)
|
|
253
|
+
return PromptResponse(stop_reason="refusal")
|
|
254
|
+
|
|
255
|
+
async def fork_session(
|
|
256
|
+
self,
|
|
257
|
+
cwd: str,
|
|
258
|
+
session_id: str,
|
|
259
|
+
mcp_servers: Optional[
|
|
260
|
+
List[HttpMcpServer | SseMcpServer | McpServerStdio]
|
|
261
|
+
] = None,
|
|
262
|
+
**kwargs: Any,
|
|
263
|
+
) -> ForkSessionResponse:
|
|
264
|
+
"""Fork an existing session."""
|
|
265
|
+
new_session_id = str(uuid.uuid4())
|
|
266
|
+
|
|
267
|
+
# Copy session state (simplified)
|
|
268
|
+
if session_id in self.sessions:
|
|
269
|
+
old_session = self.sessions[session_id]
|
|
270
|
+
new_session = ACPSession(
|
|
271
|
+
session_id=new_session_id,
|
|
272
|
+
cwd=cwd,
|
|
273
|
+
client=self.client,
|
|
274
|
+
mcp_servers=mcp_servers or old_session.mcp_servers,
|
|
275
|
+
skip_permissions=self.skip_permissions,
|
|
276
|
+
)
|
|
277
|
+
await new_session.initialize()
|
|
278
|
+
self.sessions[new_session_id] = new_session
|
|
279
|
+
self._cancel_events[new_session_id] = asyncio.Event()
|
|
280
|
+
|
|
281
|
+
return ForkSessionResponse(session_id=new_session_id)
|
|
282
|
+
|
|
283
|
+
async def resume_session(
|
|
284
|
+
self,
|
|
285
|
+
cwd: str,
|
|
286
|
+
session_id: str,
|
|
287
|
+
mcp_servers: Optional[
|
|
288
|
+
List[HttpMcpServer | SseMcpServer | McpServerStdio]
|
|
289
|
+
] = None,
|
|
290
|
+
**kwargs: Any,
|
|
291
|
+
) -> ResumeSessionResponse:
|
|
292
|
+
"""Resume an existing session."""
|
|
293
|
+
if session_id in self.sessions:
|
|
294
|
+
return ResumeSessionResponse()
|
|
295
|
+
|
|
296
|
+
# Create new session with the given ID
|
|
297
|
+
session = ACPSession(
|
|
298
|
+
session_id=session_id,
|
|
299
|
+
cwd=cwd,
|
|
300
|
+
client=self.client,
|
|
301
|
+
mcp_servers=mcp_servers or [],
|
|
302
|
+
skip_permissions=self.skip_permissions,
|
|
303
|
+
)
|
|
304
|
+
await session.initialize()
|
|
305
|
+
self.sessions[session_id] = session
|
|
306
|
+
self._cancel_events[session_id] = asyncio.Event()
|
|
307
|
+
|
|
308
|
+
return ResumeSessionResponse()
|
|
309
|
+
|
|
310
|
+
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
|
311
|
+
"""Cancel the current operation in a session."""
|
|
312
|
+
logger.info(f"Cancelling session {session_id}")
|
|
313
|
+
cancel_event = self._cancel_events.get(session_id)
|
|
314
|
+
if cancel_event:
|
|
315
|
+
cancel_event.set()
|
|
316
|
+
|
|
317
|
+
async def ext_method(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
318
|
+
"""Handle extension method (not implemented)."""
|
|
319
|
+
return {}
|
|
320
|
+
|
|
321
|
+
async def ext_notification(self, method: str, params: Dict[str, Any]) -> None:
|
|
322
|
+
"""Handle extension notification (not implemented)."""
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ACPSession:
|
|
327
|
+
"""
|
|
328
|
+
Represents an ACP session with an underlying MinionCodeAgent.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(
|
|
332
|
+
self,
|
|
333
|
+
session_id: str,
|
|
334
|
+
cwd: str,
|
|
335
|
+
client: Optional[Client],
|
|
336
|
+
mcp_servers: List[Any],
|
|
337
|
+
skip_permissions: bool = False,
|
|
338
|
+
model: Optional[str] = None,
|
|
339
|
+
):
|
|
340
|
+
self.session_id = session_id
|
|
341
|
+
self.cwd = cwd
|
|
342
|
+
self.client = client
|
|
343
|
+
self.mcp_servers = mcp_servers
|
|
344
|
+
self.skip_permissions = skip_permissions
|
|
345
|
+
self.model = model
|
|
346
|
+
self.agent: Optional[MinionCodeAgent] = None
|
|
347
|
+
self.hooks: Optional[HookConfig] = None
|
|
348
|
+
self.permission_store: Optional[PermissionStore] = None
|
|
349
|
+
self._message_history: List[Dict[str, Any]] = []
|
|
350
|
+
# Track current code execution tool call ID for pairing start/result
|
|
351
|
+
self._current_code_call_id: Optional[str] = None
|
|
352
|
+
|
|
353
|
+
async def initialize(self) -> None:
|
|
354
|
+
"""Initialize the session and create the agent."""
|
|
355
|
+
# Create permission store for this project
|
|
356
|
+
self.permission_store = PermissionStore(cwd=self.cwd)
|
|
357
|
+
|
|
358
|
+
# Create ACP hooks
|
|
359
|
+
if self.client:
|
|
360
|
+
self.hooks = create_acp_hooks(
|
|
361
|
+
client=self.client,
|
|
362
|
+
session_id=self.session_id,
|
|
363
|
+
request_permission=not self.skip_permissions, # Ask user permission unless skipped
|
|
364
|
+
include_dangerous_check=True,
|
|
365
|
+
permission_store=self.permission_store,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Create the agent with optional model override
|
|
369
|
+
create_kwargs = {
|
|
370
|
+
"hooks": self.hooks,
|
|
371
|
+
"workdir": self.cwd,
|
|
372
|
+
# History decay: save large outputs to file after N steps
|
|
373
|
+
"decay_enabled": True,
|
|
374
|
+
"decay_ttl_steps": 3,
|
|
375
|
+
"decay_min_size": 100_000, # 100KB
|
|
376
|
+
}
|
|
377
|
+
if self.model:
|
|
378
|
+
create_kwargs["llm"] = self.model
|
|
379
|
+
logger.info(f"Creating agent with model: {self.model}")
|
|
380
|
+
|
|
381
|
+
self.agent = await MinionCodeAgent.create(**create_kwargs)
|
|
382
|
+
|
|
383
|
+
async def run_prompt(
|
|
384
|
+
self,
|
|
385
|
+
message: str,
|
|
386
|
+
cancel_event: Optional[asyncio.Event] = None,
|
|
387
|
+
) -> str:
|
|
388
|
+
"""
|
|
389
|
+
Run a prompt through the agent and stream results.
|
|
390
|
+
|
|
391
|
+
Returns the stop reason for the prompt response.
|
|
392
|
+
"""
|
|
393
|
+
if not self.agent or not self.client:
|
|
394
|
+
return "refusal"
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
# Run agent with streaming - await to get async generator, then iterate
|
|
398
|
+
stream = await self.agent.run_async(message, stream=True)
|
|
399
|
+
async for chunk in stream:
|
|
400
|
+
# Check for cancellation
|
|
401
|
+
if cancel_event and cancel_event.is_set():
|
|
402
|
+
return "cancelled"
|
|
403
|
+
|
|
404
|
+
# Handle different chunk types
|
|
405
|
+
await self._handle_stream_chunk(chunk)
|
|
406
|
+
|
|
407
|
+
return "end_turn"
|
|
408
|
+
|
|
409
|
+
except asyncio.CancelledError:
|
|
410
|
+
return "cancelled"
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Error in run_prompt: {e}")
|
|
413
|
+
return "refusal"
|
|
414
|
+
|
|
415
|
+
async def _handle_stream_chunk(self, chunk: Any) -> None:
|
|
416
|
+
"""Handle a stream chunk from the agent and convert to ACP events."""
|
|
417
|
+
if not self.client:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
# Import StreamChunk type
|
|
421
|
+
from minion.types import StreamChunk, AgentResponse
|
|
422
|
+
|
|
423
|
+
if isinstance(chunk, StreamChunk):
|
|
424
|
+
chunk_type = chunk.chunk_type
|
|
425
|
+
content = chunk.content
|
|
426
|
+
metadata = getattr(chunk, "metadata", {}) or {}
|
|
427
|
+
|
|
428
|
+
if chunk_type == "thinking":
|
|
429
|
+
# LLM reasoning/thinking - send as thought chunk
|
|
430
|
+
if content:
|
|
431
|
+
await self.client.session_update(
|
|
432
|
+
session_id=self.session_id,
|
|
433
|
+
update=update_agent_thought_text(content),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
elif chunk_type in ("text", "content"):
|
|
437
|
+
# Regular assistant message content
|
|
438
|
+
if content:
|
|
439
|
+
await self.client.session_update(
|
|
440
|
+
session_id=self.session_id,
|
|
441
|
+
update=update_agent_message_text(content),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
elif chunk_type == "code_start":
|
|
445
|
+
# Code execution starting - send ToolCallStart
|
|
446
|
+
self._current_code_call_id = str(uuid.uuid4())
|
|
447
|
+
await self.client.session_update(
|
|
448
|
+
session_id=self.session_id,
|
|
449
|
+
update=ToolCallStart(
|
|
450
|
+
session_update="tool_call",
|
|
451
|
+
tool_call_id=self._current_code_call_id,
|
|
452
|
+
title="Executing Python code",
|
|
453
|
+
kind="execute",
|
|
454
|
+
status="in_progress",
|
|
455
|
+
raw_input=f"```python\n{content}\n```",
|
|
456
|
+
content=[
|
|
457
|
+
ContentToolCallContent(
|
|
458
|
+
type="content",
|
|
459
|
+
content=TextContentBlock(
|
|
460
|
+
type="text", text=f"```python\n{content}\n```"
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
],
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
elif chunk_type == "code_result":
|
|
468
|
+
# Code execution result - send ToolCallProgress
|
|
469
|
+
if self._current_code_call_id:
|
|
470
|
+
success = metadata.get("success", True)
|
|
471
|
+
await self.client.session_update(
|
|
472
|
+
session_id=self.session_id,
|
|
473
|
+
update=ToolCallProgress(
|
|
474
|
+
session_update="tool_call_update",
|
|
475
|
+
tool_call_id=self._current_code_call_id,
|
|
476
|
+
status="completed" if success else "failed",
|
|
477
|
+
content=[
|
|
478
|
+
ContentToolCallContent(
|
|
479
|
+
type="content",
|
|
480
|
+
content=TextContentBlock(
|
|
481
|
+
type="text",
|
|
482
|
+
text=(
|
|
483
|
+
content
|
|
484
|
+
if content
|
|
485
|
+
else "Executed successfully"
|
|
486
|
+
),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
],
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
self._current_code_call_id = None
|
|
493
|
+
|
|
494
|
+
elif chunk_type == "step_start":
|
|
495
|
+
# Step start notification - can be logged or sent as info
|
|
496
|
+
logger.debug(f"Step started: {metadata.get('iteration', '?')}")
|
|
497
|
+
|
|
498
|
+
elif chunk_type == "tool_call":
|
|
499
|
+
# Direct tool call (non-code execution) - handled by pre_tool_use hook
|
|
500
|
+
# But we can also send notification here if needed
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
elif chunk_type == "tool_response":
|
|
504
|
+
# Tool response - handled by post_tool_use hook
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
elif chunk_type == "error":
|
|
508
|
+
# Error message
|
|
509
|
+
if content:
|
|
510
|
+
await self.client.session_update(
|
|
511
|
+
session_id=self.session_id,
|
|
512
|
+
update=update_agent_message_text(f"Error: {content}"),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
elif chunk_type == "final_answer":
|
|
516
|
+
# Final answer reached
|
|
517
|
+
if content:
|
|
518
|
+
await self.client.session_update(
|
|
519
|
+
session_id=self.session_id,
|
|
520
|
+
update=update_agent_message_text(content),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
elif isinstance(chunk, AgentResponse):
|
|
524
|
+
# Final AgentResponse with answer
|
|
525
|
+
if chunk.answer:
|
|
526
|
+
await self.client.session_update(
|
|
527
|
+
session_id=self.session_id,
|
|
528
|
+
update=update_agent_message_text(chunk.answer),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
elif hasattr(chunk, "answer") and chunk.answer:
|
|
532
|
+
# Fallback for objects with answer attribute
|
|
533
|
+
await self.client.session_update(
|
|
534
|
+
session_id=self.session_id,
|
|
535
|
+
update=update_agent_message_text(chunk.answer),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
__all__ = ["MinionACPAgent", "ACPSession"]
|