ripperdoc 0.2.8__py3-none-any.whl → 0.2.10__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +257 -123
- ripperdoc/cli/commands/__init__.py +2 -1
- ripperdoc/cli/commands/agents_cmd.py +138 -8
- ripperdoc/cli/commands/clear_cmd.py +9 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +27 -10
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +9 -3
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +403 -81
- ripperdoc/cli/ui/spinner.py +54 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +213 -0
- ripperdoc/core/agents.py +19 -6
- ripperdoc/core/config.py +51 -17
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +101 -12
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +27 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +118 -12
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +89 -24
- ripperdoc/core/query.py +273 -68
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +17 -8
- ripperdoc/sdk/client.py +79 -4
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +307 -135
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +63 -24
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +167 -54
- ripperdoc/tools/file_read_tool.py +28 -4
- ripperdoc/tools/file_write_tool.py +13 -10
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +519 -69
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +9 -5
- ripperdoc/utils/file_watch.py +214 -5
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +14 -7
- ripperdoc/utils/messages.py +126 -67
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/session_stats.py +293 -0
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- ripperdoc-0.2.10.dist-info/RECORD +129 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/tools/task_tool.py
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
5
8
|
import time
|
|
6
|
-
from
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, AsyncGenerator, Callable, Dict, Iterable, List, Optional, Sequence
|
|
11
|
+
from uuid import uuid4
|
|
7
12
|
|
|
8
13
|
from pydantic import BaseModel, Field
|
|
9
14
|
|
|
@@ -20,29 +25,172 @@ from ripperdoc.core.agents import (
|
|
|
20
25
|
)
|
|
21
26
|
from ripperdoc.core.query import QueryContext, query
|
|
22
27
|
from ripperdoc.core.system_prompt import build_environment_prompt
|
|
23
|
-
from ripperdoc.core.tool import
|
|
24
|
-
|
|
28
|
+
from ripperdoc.core.tool import (
|
|
29
|
+
Tool,
|
|
30
|
+
ToolOutput,
|
|
31
|
+
ToolProgress,
|
|
32
|
+
ToolResult,
|
|
33
|
+
ToolUseContext,
|
|
34
|
+
ValidationResult,
|
|
35
|
+
)
|
|
36
|
+
from ripperdoc.utils.messages import AssistantMessage, UserMessage, create_user_message
|
|
25
37
|
from ripperdoc.utils.log import get_logger
|
|
26
38
|
|
|
27
39
|
logger = get_logger()
|
|
28
40
|
|
|
29
41
|
|
|
42
|
+
MessageType = UserMessage | AssistantMessage
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class AgentRunRecord:
|
|
47
|
+
"""In-memory record for a subagent run (foreground or background)."""
|
|
48
|
+
|
|
49
|
+
agent_id: str
|
|
50
|
+
agent_type: str
|
|
51
|
+
tools: List[Tool[Any, Any]]
|
|
52
|
+
system_prompt: str
|
|
53
|
+
history: List[MessageType]
|
|
54
|
+
missing_tools: List[str]
|
|
55
|
+
model_used: Optional[str]
|
|
56
|
+
start_time: float
|
|
57
|
+
duration_ms: float = 0.0
|
|
58
|
+
tool_use_count: int = 0
|
|
59
|
+
status: str = "running"
|
|
60
|
+
result_text: Optional[str] = None
|
|
61
|
+
error: Optional[str] = None
|
|
62
|
+
task: Optional[asyncio.Task] = None
|
|
63
|
+
is_background: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_AGENT_RUNS: Dict[str, AgentRunRecord] = {}
|
|
67
|
+
_AGENT_RUNS_LOCK = threading.Lock()
|
|
68
|
+
DEFAULT_AGENT_RUN_TTL_SEC = float(os.getenv("RIPPERDOC_AGENT_RUN_TTL_SEC", "3600"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _new_agent_id() -> str:
|
|
72
|
+
return f"agent_{uuid4().hex[:8]}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _register_agent_run(record: AgentRunRecord) -> None:
|
|
76
|
+
with _AGENT_RUNS_LOCK:
|
|
77
|
+
_AGENT_RUNS[record.agent_id] = record
|
|
78
|
+
prune_agent_runs()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_agent_run(agent_id: str) -> Optional[AgentRunRecord]:
|
|
82
|
+
with _AGENT_RUNS_LOCK:
|
|
83
|
+
return _AGENT_RUNS.get(agent_id)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _snapshot_agent_run(record: AgentRunRecord) -> dict:
|
|
87
|
+
duration_ms = (
|
|
88
|
+
record.duration_ms
|
|
89
|
+
if record.duration_ms
|
|
90
|
+
else max((time.time() - record.start_time) * 1000.0, 0.0)
|
|
91
|
+
)
|
|
92
|
+
return {
|
|
93
|
+
"id": record.agent_id,
|
|
94
|
+
"agent_type": record.agent_type,
|
|
95
|
+
"status": record.status,
|
|
96
|
+
"duration_ms": duration_ms,
|
|
97
|
+
"tool_use_count": record.tool_use_count,
|
|
98
|
+
"missing_tools": list(record.missing_tools),
|
|
99
|
+
"model_used": record.model_used,
|
|
100
|
+
"result_text": record.result_text,
|
|
101
|
+
"error": record.error,
|
|
102
|
+
"is_background": record.is_background,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def list_agent_runs() -> List[str]:
|
|
107
|
+
"""Return known subagent run ids."""
|
|
108
|
+
prune_agent_runs()
|
|
109
|
+
with _AGENT_RUNS_LOCK:
|
|
110
|
+
return list(_AGENT_RUNS.keys())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_agent_run_snapshot(agent_id: str) -> Optional[dict]:
|
|
114
|
+
"""Return a snapshot of a subagent run by id."""
|
|
115
|
+
record = _get_agent_run(agent_id)
|
|
116
|
+
if not record:
|
|
117
|
+
return None
|
|
118
|
+
return _snapshot_agent_run(record)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def prune_agent_runs(max_age_seconds: Optional[float] = None) -> int:
|
|
122
|
+
"""Remove finished subagent runs older than the TTL."""
|
|
123
|
+
ttl = DEFAULT_AGENT_RUN_TTL_SEC if max_age_seconds is None else max_age_seconds
|
|
124
|
+
if ttl is None or ttl <= 0:
|
|
125
|
+
return 0
|
|
126
|
+
now = time.time()
|
|
127
|
+
removed = 0
|
|
128
|
+
with _AGENT_RUNS_LOCK:
|
|
129
|
+
for agent_id, record in list(_AGENT_RUNS.items()):
|
|
130
|
+
if record.status == "running" or record.task:
|
|
131
|
+
continue
|
|
132
|
+
age = now - record.start_time
|
|
133
|
+
if age > ttl:
|
|
134
|
+
_AGENT_RUNS.pop(agent_id, None)
|
|
135
|
+
removed += 1
|
|
136
|
+
return removed
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def cancel_agent_run(agent_id: str) -> bool:
|
|
140
|
+
"""Cancel a running subagent, if possible."""
|
|
141
|
+
record = _get_agent_run(agent_id)
|
|
142
|
+
if not record or not record.task or record.task.done():
|
|
143
|
+
return False
|
|
144
|
+
record.task.cancel()
|
|
145
|
+
try:
|
|
146
|
+
await record.task
|
|
147
|
+
except asyncio.CancelledError:
|
|
148
|
+
pass
|
|
149
|
+
record.status = "cancelled"
|
|
150
|
+
record.error = record.error or "Cancelled by user."
|
|
151
|
+
record.duration_ms = (time.time() - record.start_time) * 1000
|
|
152
|
+
record.task = None
|
|
153
|
+
return True
|
|
154
|
+
|
|
30
155
|
class TaskToolInput(BaseModel):
|
|
31
156
|
"""Input schema for delegating to a subagent."""
|
|
32
157
|
|
|
33
|
-
prompt: str = Field(
|
|
34
|
-
|
|
158
|
+
prompt: Optional[str] = Field(
|
|
159
|
+
default=None,
|
|
160
|
+
description="Detailed task description for the subagent to perform.",
|
|
161
|
+
)
|
|
162
|
+
subagent_type: Optional[str] = Field(
|
|
163
|
+
default=None,
|
|
164
|
+
description="Agent type to run (matches agent frontmatter name). Required for new runs.",
|
|
165
|
+
)
|
|
166
|
+
run_in_background: bool = Field(
|
|
167
|
+
default=False,
|
|
168
|
+
description="If true, start the agent in the background and return immediately.",
|
|
169
|
+
)
|
|
170
|
+
resume: Optional[str] = Field(
|
|
171
|
+
default=None,
|
|
172
|
+
description="Agent id to resume or fetch results for a background run.",
|
|
173
|
+
)
|
|
174
|
+
wait: bool = Field(
|
|
175
|
+
default=True,
|
|
176
|
+
description="When resuming a background agent, wait for completion before returning.",
|
|
177
|
+
)
|
|
35
178
|
|
|
36
179
|
|
|
37
180
|
class TaskToolOutput(BaseModel):
|
|
38
181
|
"""Summary of a completed subagent run."""
|
|
39
182
|
|
|
183
|
+
agent_id: Optional[str] = None
|
|
40
184
|
agent_type: str
|
|
41
185
|
result_text: str
|
|
42
186
|
duration_ms: float
|
|
43
187
|
tool_use_count: int
|
|
44
188
|
missing_tools: List[str] = Field(default_factory=list)
|
|
45
189
|
model_used: Optional[str] = None
|
|
190
|
+
status: str = "completed"
|
|
191
|
+
is_background: bool = False
|
|
192
|
+
is_resumed: bool = False
|
|
193
|
+
error: Optional[str] = None
|
|
46
194
|
|
|
47
195
|
|
|
48
196
|
class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
@@ -69,8 +217,8 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
69
217
|
def input_schema(self) -> type[TaskToolInput]:
|
|
70
218
|
return TaskToolInput
|
|
71
219
|
|
|
72
|
-
async def prompt(self,
|
|
73
|
-
del
|
|
220
|
+
async def prompt(self, yolo_mode: bool = False) -> str:
|
|
221
|
+
del yolo_mode
|
|
74
222
|
clear_agent_cache()
|
|
75
223
|
agents: AgentLoadResult = load_agent_definitions()
|
|
76
224
|
|
|
@@ -101,7 +249,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
101
249
|
f"The {task_tool_name} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\n"
|
|
102
250
|
f"Available agent types and the tools they have access to:\n"
|
|
103
251
|
f"{agent_block}\n\n"
|
|
104
|
-
f"When
|
|
252
|
+
f"When starting a new agent with the {task_tool_name} tool, you must specify a subagent_type parameter to select which agent type to use.\n\n"
|
|
105
253
|
f"When NOT to use the {task_tool_name} tool:\n"
|
|
106
254
|
f"- If you want to read a specific file path, use the {file_read_tool_name} or {search_tool_name} tool instead of the {task_tool_name} tool, to find the match more quickly\n"
|
|
107
255
|
f'- If you are searching for a specific class definition like "class Foo", use the {search_tool_name} tool instead, to find the match more quickly\n'
|
|
@@ -112,11 +260,11 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
112
260
|
"Usage notes:\n"
|
|
113
261
|
"- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n"
|
|
114
262
|
"- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n"
|
|
115
|
-
f"-
|
|
116
|
-
"-
|
|
117
|
-
"-
|
|
263
|
+
f"- Use run_in_background=true to launch an agent asynchronously. The tool will return an agent_id immediately for later retrieval.\n"
|
|
264
|
+
f"- Fetch background results by calling {background_fetch_tool_name} with resume=<agent_id>. If the agent is still running, set wait=true to block or wait=false to get status only.\n"
|
|
265
|
+
"- To continue a completed agent, call Task with resume=<agent_id> and a new prompt.\n"
|
|
118
266
|
"- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n"
|
|
119
|
-
'- Agents
|
|
267
|
+
'- Agents can opt into parent context by setting fork_context: true in their frontmatter. When enabled, they receive the full conversation history before the tool call.\n'
|
|
120
268
|
"- The agent's outputs should generally be trusted\n"
|
|
121
269
|
"- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n"
|
|
122
270
|
"- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n"
|
|
@@ -125,7 +273,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
125
273
|
"Example usage:\n"
|
|
126
274
|
"\n"
|
|
127
275
|
"<example_agent_descriptions>\n"
|
|
128
|
-
'"code-reviewer": use this agent after you are done writing a
|
|
276
|
+
'"code-reviewer": use this agent after you are done writing a significant piece of code\n'
|
|
129
277
|
'"greeting-responder": use this agent when to respond to user greetings with a friendly joke\n'
|
|
130
278
|
"</example_agent_description>\n"
|
|
131
279
|
"\n"
|
|
@@ -144,7 +292,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
144
292
|
"}\n"
|
|
145
293
|
"</code>\n"
|
|
146
294
|
"<commentary>\n"
|
|
147
|
-
"Since a
|
|
295
|
+
"Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code\n"
|
|
148
296
|
"</commentary>\n"
|
|
149
297
|
"assistant: Now let me use the code-reviewer agent to review the code\n"
|
|
150
298
|
f"assistant: Uses the {task_tool_name} tool to launch the code-reviewer agent \n"
|
|
@@ -165,20 +313,60 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
165
313
|
def is_concurrency_safe(self) -> bool:
|
|
166
314
|
return True
|
|
167
315
|
|
|
316
|
+
async def validate_input(
|
|
317
|
+
self, input_data: TaskToolInput, context: Optional[ToolUseContext] = None
|
|
318
|
+
) -> ValidationResult:
|
|
319
|
+
del context
|
|
320
|
+
if input_data.resume and input_data.run_in_background:
|
|
321
|
+
return ValidationResult(
|
|
322
|
+
result=False,
|
|
323
|
+
message="run_in_background cannot be used when resuming an agent.",
|
|
324
|
+
)
|
|
325
|
+
if input_data.resume:
|
|
326
|
+
if input_data.prompt is not None and not input_data.prompt.strip():
|
|
327
|
+
return ValidationResult(
|
|
328
|
+
result=False,
|
|
329
|
+
message="prompt cannot be empty when resuming with new work.",
|
|
330
|
+
)
|
|
331
|
+
return ValidationResult(result=True)
|
|
332
|
+
|
|
333
|
+
if not input_data.subagent_type:
|
|
334
|
+
return ValidationResult(
|
|
335
|
+
result=False,
|
|
336
|
+
message="subagent_type is required when starting a new agent.",
|
|
337
|
+
)
|
|
338
|
+
if not input_data.prompt or not input_data.prompt.strip():
|
|
339
|
+
return ValidationResult(
|
|
340
|
+
result=False,
|
|
341
|
+
message="prompt is required when starting a new agent.",
|
|
342
|
+
)
|
|
343
|
+
return ValidationResult(result=True)
|
|
344
|
+
|
|
168
345
|
def render_result_for_assistant(self, output: TaskToolOutput) -> str:
|
|
169
346
|
details: List[str] = []
|
|
347
|
+
if output.agent_id:
|
|
348
|
+
details.append(f"id={output.agent_id}")
|
|
349
|
+
if output.status and output.status != "completed":
|
|
350
|
+
details.append(output.status)
|
|
170
351
|
if output.tool_use_count:
|
|
171
352
|
details.append(f"{output.tool_use_count} tool uses")
|
|
172
353
|
details.append(f"{output.duration_ms / 1000:.1f}s")
|
|
173
354
|
if output.missing_tools:
|
|
174
355
|
details.append(f"missing tools: {', '.join(output.missing_tools)}")
|
|
356
|
+
if output.error:
|
|
357
|
+
details.append(f"error: {output.error}")
|
|
175
358
|
|
|
176
359
|
suffix = f" ({'; '.join(details)})" if details else ""
|
|
177
360
|
return f"[subagent:{output.agent_type}] {output.result_text}{suffix}"
|
|
178
361
|
|
|
179
362
|
def render_tool_use_message(self, input_data: TaskToolInput, verbose: bool = False) -> str:
|
|
180
363
|
del verbose
|
|
181
|
-
|
|
364
|
+
if input_data.resume:
|
|
365
|
+
return f"Resume subagent {input_data.resume}"
|
|
366
|
+
label = f"Task via {input_data.subagent_type}: {input_data.prompt}"
|
|
367
|
+
if input_data.run_in_background:
|
|
368
|
+
label += " (background)"
|
|
369
|
+
return label
|
|
182
370
|
|
|
183
371
|
async def call(
|
|
184
372
|
self,
|
|
@@ -187,6 +375,111 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
187
375
|
) -> AsyncGenerator[ToolOutput, None]:
|
|
188
376
|
clear_agent_cache()
|
|
189
377
|
agents = load_agent_definitions()
|
|
378
|
+
|
|
379
|
+
if input_data.resume:
|
|
380
|
+
record = _get_agent_run(input_data.resume)
|
|
381
|
+
if not record:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"Agent id '{input_data.resume}' not found. "
|
|
384
|
+
"Start a new agent to obtain a valid agent_id."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if record.task and not record.task.done():
|
|
388
|
+
if not input_data.wait:
|
|
389
|
+
output = self._output_from_record(
|
|
390
|
+
record,
|
|
391
|
+
status_override="running",
|
|
392
|
+
result_text_override="Agent is still running in the background.",
|
|
393
|
+
is_background=True,
|
|
394
|
+
is_resumed=True,
|
|
395
|
+
)
|
|
396
|
+
yield ToolResult(
|
|
397
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
398
|
+
)
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
yield ToolProgress(
|
|
402
|
+
content=f"Waiting for subagent '{record.agent_type}' ({record.agent_id})"
|
|
403
|
+
)
|
|
404
|
+
try:
|
|
405
|
+
await record.task
|
|
406
|
+
except asyncio.CancelledError:
|
|
407
|
+
raise
|
|
408
|
+
except Exception as exc:
|
|
409
|
+
record.status = "failed"
|
|
410
|
+
record.error = str(exc)
|
|
411
|
+
|
|
412
|
+
if not input_data.prompt:
|
|
413
|
+
output = self._output_from_record(
|
|
414
|
+
record,
|
|
415
|
+
is_background=bool(record.task),
|
|
416
|
+
is_resumed=True,
|
|
417
|
+
)
|
|
418
|
+
yield ToolResult(
|
|
419
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
record.history.append(create_user_message(input_data.prompt))
|
|
424
|
+
record.start_time = time.time()
|
|
425
|
+
record.duration_ms = 0.0
|
|
426
|
+
record.tool_use_count = 0
|
|
427
|
+
record.status = "running"
|
|
428
|
+
record.result_text = None
|
|
429
|
+
record.error = None
|
|
430
|
+
record.task = None
|
|
431
|
+
|
|
432
|
+
subagent_context = QueryContext(
|
|
433
|
+
tools=record.tools,
|
|
434
|
+
yolo_mode=context.yolo_mode,
|
|
435
|
+
verbose=context.verbose,
|
|
436
|
+
model=record.model_used or "main",
|
|
437
|
+
stop_hook="subagent",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
yield ToolProgress(content=f"Resuming subagent '{record.agent_type}'")
|
|
441
|
+
|
|
442
|
+
assistant_messages: List[AssistantMessage] = []
|
|
443
|
+
tool_use_count = 0
|
|
444
|
+
async for message in query(
|
|
445
|
+
record.history, # type: ignore[arg-type]
|
|
446
|
+
record.system_prompt,
|
|
447
|
+
{},
|
|
448
|
+
subagent_context,
|
|
449
|
+
context.permission_checker,
|
|
450
|
+
):
|
|
451
|
+
if getattr(message, "type", "") == "progress":
|
|
452
|
+
continue
|
|
453
|
+
tool_use_count, updates = self._track_subagent_message(
|
|
454
|
+
message,
|
|
455
|
+
record.history,
|
|
456
|
+
assistant_messages,
|
|
457
|
+
tool_use_count,
|
|
458
|
+
)
|
|
459
|
+
for update in updates:
|
|
460
|
+
yield ToolProgress(content=update)
|
|
461
|
+
|
|
462
|
+
duration_ms = (time.time() - record.start_time) * 1000
|
|
463
|
+
result_text = (
|
|
464
|
+
self._extract_text(assistant_messages[-1])
|
|
465
|
+
if assistant_messages
|
|
466
|
+
else "Agent returned no response."
|
|
467
|
+
)
|
|
468
|
+
record.duration_ms = duration_ms
|
|
469
|
+
record.tool_use_count = tool_use_count
|
|
470
|
+
record.result_text = result_text.strip()
|
|
471
|
+
record.status = "completed"
|
|
472
|
+
|
|
473
|
+
output = self._output_from_record(
|
|
474
|
+
record,
|
|
475
|
+
result_text_override=result_text.strip(),
|
|
476
|
+
is_resumed=True,
|
|
477
|
+
)
|
|
478
|
+
yield ToolResult(
|
|
479
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
480
|
+
)
|
|
481
|
+
return
|
|
482
|
+
|
|
190
483
|
target_agent = next(
|
|
191
484
|
(
|
|
192
485
|
agent
|
|
@@ -217,83 +510,239 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
217
510
|
]
|
|
218
511
|
|
|
219
512
|
agent_system_prompt = self._build_agent_prompt(target_agent, typed_agent_tools)
|
|
220
|
-
|
|
513
|
+
parent_history = (
|
|
514
|
+
self._coerce_parent_history(getattr(context, "conversation_messages", None))
|
|
515
|
+
if target_agent.fork_context
|
|
516
|
+
else []
|
|
517
|
+
)
|
|
518
|
+
subagent_messages = [
|
|
519
|
+
*parent_history,
|
|
520
|
+
create_user_message(input_data.prompt or ""),
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
agent_id = _new_agent_id()
|
|
524
|
+
record = AgentRunRecord(
|
|
525
|
+
agent_id=agent_id,
|
|
526
|
+
agent_type=target_agent.agent_type,
|
|
527
|
+
tools=typed_agent_tools,
|
|
528
|
+
system_prompt=agent_system_prompt,
|
|
529
|
+
history=subagent_messages,
|
|
530
|
+
missing_tools=missing_tools,
|
|
531
|
+
model_used=target_agent.model or "main",
|
|
532
|
+
start_time=time.time(),
|
|
533
|
+
is_background=bool(input_data.run_in_background),
|
|
534
|
+
)
|
|
535
|
+
_register_agent_run(record)
|
|
221
536
|
|
|
222
537
|
subagent_context = QueryContext(
|
|
223
538
|
tools=typed_agent_tools,
|
|
224
|
-
|
|
539
|
+
yolo_mode=context.yolo_mode,
|
|
225
540
|
verbose=context.verbose,
|
|
226
|
-
model=target_agent.model or "
|
|
541
|
+
model=target_agent.model or "main",
|
|
542
|
+
stop_hook="subagent",
|
|
227
543
|
)
|
|
228
544
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
545
|
+
if input_data.run_in_background:
|
|
546
|
+
record.task = asyncio.create_task(
|
|
547
|
+
self._run_subagent_background(
|
|
548
|
+
record,
|
|
549
|
+
subagent_context,
|
|
550
|
+
context.permission_checker,
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
output = self._output_from_record(
|
|
554
|
+
record,
|
|
555
|
+
status_override="running",
|
|
556
|
+
result_text_override="Agent started in the background.",
|
|
557
|
+
is_background=True,
|
|
558
|
+
)
|
|
559
|
+
yield ToolResult(
|
|
560
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
561
|
+
)
|
|
562
|
+
return
|
|
232
563
|
|
|
233
564
|
yield ToolProgress(content=f"Launching subagent '{target_agent.agent_type}'")
|
|
234
565
|
|
|
566
|
+
assistant_messages = []
|
|
567
|
+
tool_use_count = 0
|
|
235
568
|
async for message in query(
|
|
236
|
-
|
|
569
|
+
record.history, # type: ignore[arg-type]
|
|
237
570
|
agent_system_prompt,
|
|
238
571
|
{},
|
|
239
572
|
subagent_context,
|
|
240
573
|
context.permission_checker,
|
|
241
574
|
):
|
|
242
|
-
if getattr(message, "type", "") == "
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
block_input = (
|
|
256
|
-
getattr(block, "input", None)
|
|
257
|
-
if hasattr(block, "input")
|
|
258
|
-
else (block.get("input") if isinstance(block, Dict) else None)
|
|
259
|
-
)
|
|
260
|
-
summary = self._summarize_tool_input(block_input)
|
|
261
|
-
label = f"Subagent requesting {tool_name}"
|
|
262
|
-
if summary:
|
|
263
|
-
label += f" — {summary}"
|
|
264
|
-
yield ToolProgress(content=label)
|
|
265
|
-
if block_type == "text":
|
|
266
|
-
text_val = getattr(block, "text", None) or (
|
|
267
|
-
block.get("text") if isinstance(block, Dict) else ""
|
|
268
|
-
)
|
|
269
|
-
if text_val:
|
|
270
|
-
snippet = str(text_val).strip()
|
|
271
|
-
if snippet:
|
|
272
|
-
short = (
|
|
273
|
-
snippet if len(snippet) <= 200 else snippet[:197] + "..."
|
|
274
|
-
)
|
|
275
|
-
yield ToolProgress(content=f"Subagent: {short}")
|
|
276
|
-
assistant_messages.append(message) # type: ignore[arg-type]
|
|
277
|
-
if isinstance(message, AssistantMessage):
|
|
278
|
-
tool_use_count += self._count_tool_uses(message)
|
|
279
|
-
|
|
280
|
-
duration_ms = (time.time() - start) * 1000
|
|
575
|
+
if getattr(message, "type", "") == "progress":
|
|
576
|
+
continue
|
|
577
|
+
tool_use_count, updates = self._track_subagent_message(
|
|
578
|
+
message,
|
|
579
|
+
record.history,
|
|
580
|
+
assistant_messages,
|
|
581
|
+
tool_use_count,
|
|
582
|
+
)
|
|
583
|
+
for update in updates:
|
|
584
|
+
yield ToolProgress(content=update)
|
|
585
|
+
|
|
586
|
+
duration_ms = (time.time() - record.start_time) * 1000
|
|
281
587
|
result_text = (
|
|
282
588
|
self._extract_text(assistant_messages[-1])
|
|
283
589
|
if assistant_messages
|
|
284
590
|
else "Agent returned no response."
|
|
285
591
|
)
|
|
286
592
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
593
|
+
record.duration_ms = duration_ms
|
|
594
|
+
record.tool_use_count = tool_use_count
|
|
595
|
+
record.result_text = result_text.strip()
|
|
596
|
+
record.status = "completed"
|
|
597
|
+
|
|
598
|
+
output = self._output_from_record(record, result_text_override=result_text.strip())
|
|
599
|
+
|
|
600
|
+
yield ToolResult(data=output, result_for_assistant=self.render_result_for_assistant(output))
|
|
601
|
+
|
|
602
|
+
def _output_from_record(
|
|
603
|
+
self,
|
|
604
|
+
record: AgentRunRecord,
|
|
605
|
+
*,
|
|
606
|
+
status_override: Optional[str] = None,
|
|
607
|
+
result_text_override: Optional[str] = None,
|
|
608
|
+
is_background: bool = False,
|
|
609
|
+
is_resumed: bool = False,
|
|
610
|
+
error_override: Optional[str] = None,
|
|
611
|
+
) -> TaskToolOutput:
|
|
612
|
+
status = status_override or record.status
|
|
613
|
+
duration_ms = (
|
|
614
|
+
record.duration_ms
|
|
615
|
+
if record.duration_ms
|
|
616
|
+
else max((time.time() - record.start_time) * 1000, 0.0)
|
|
617
|
+
)
|
|
618
|
+
result_text = (
|
|
619
|
+
result_text_override
|
|
620
|
+
or record.result_text
|
|
621
|
+
or ("Agent is still running." if status == "running" else "Agent returned no response.")
|
|
622
|
+
)
|
|
623
|
+
return TaskToolOutput(
|
|
624
|
+
agent_id=record.agent_id,
|
|
625
|
+
agent_type=record.agent_type,
|
|
626
|
+
result_text=result_text,
|
|
290
627
|
duration_ms=duration_ms,
|
|
291
|
-
tool_use_count=tool_use_count,
|
|
292
|
-
missing_tools=missing_tools,
|
|
293
|
-
model_used=
|
|
628
|
+
tool_use_count=record.tool_use_count,
|
|
629
|
+
missing_tools=record.missing_tools,
|
|
630
|
+
model_used=record.model_used,
|
|
631
|
+
status=status,
|
|
632
|
+
is_background=is_background,
|
|
633
|
+
is_resumed=is_resumed,
|
|
634
|
+
error=error_override or record.error,
|
|
294
635
|
)
|
|
295
636
|
|
|
296
|
-
|
|
637
|
+
def _coerce_parent_history(
|
|
638
|
+
self, messages: Optional[Sequence[object]]
|
|
639
|
+
) -> List[MessageType]:
|
|
640
|
+
if not messages:
|
|
641
|
+
return []
|
|
642
|
+
history: List[MessageType] = []
|
|
643
|
+
for msg in messages:
|
|
644
|
+
msg_type = getattr(msg, "type", None)
|
|
645
|
+
if msg_type in ("user", "assistant"):
|
|
646
|
+
history.append(msg) # type: ignore[arg-type]
|
|
647
|
+
return history
|
|
648
|
+
|
|
649
|
+
def _track_subagent_message(
|
|
650
|
+
self,
|
|
651
|
+
message: object,
|
|
652
|
+
history: List[MessageType],
|
|
653
|
+
assistant_messages: List[AssistantMessage],
|
|
654
|
+
tool_use_count: int,
|
|
655
|
+
) -> tuple[int, List[str]]:
|
|
656
|
+
updates: List[str] = []
|
|
657
|
+
msg_type = getattr(message, "type", "")
|
|
658
|
+
if msg_type in ("assistant", "user"):
|
|
659
|
+
history.append(message) # type: ignore[arg-type]
|
|
660
|
+
|
|
661
|
+
if msg_type == "assistant":
|
|
662
|
+
if isinstance(message, AssistantMessage):
|
|
663
|
+
tool_use_count += self._count_tool_uses(message)
|
|
664
|
+
updates = self._extract_progress_updates(message)
|
|
665
|
+
assistant_messages.append(message) # type: ignore[arg-type]
|
|
666
|
+
|
|
667
|
+
return tool_use_count, updates
|
|
668
|
+
|
|
669
|
+
def _extract_progress_updates(self, message: object) -> List[str]:
|
|
670
|
+
msg_content = getattr(message, "message", None)
|
|
671
|
+
blocks = getattr(msg_content, "content", []) if msg_content else []
|
|
672
|
+
if not isinstance(blocks, list):
|
|
673
|
+
return []
|
|
674
|
+
|
|
675
|
+
updates: List[str] = []
|
|
676
|
+
for block in blocks:
|
|
677
|
+
block_type = getattr(block, "type", None) or (
|
|
678
|
+
block.get("type") if isinstance(block, Dict) else None
|
|
679
|
+
)
|
|
680
|
+
if block_type == "tool_use":
|
|
681
|
+
tool_name = getattr(block, "name", None) or (
|
|
682
|
+
block.get("name") if isinstance(block, Dict) else "unknown tool"
|
|
683
|
+
)
|
|
684
|
+
block_input = (
|
|
685
|
+
getattr(block, "input", None)
|
|
686
|
+
if hasattr(block, "input")
|
|
687
|
+
else (block.get("input") if isinstance(block, Dict) else None)
|
|
688
|
+
)
|
|
689
|
+
summary = self._summarize_tool_input(block_input)
|
|
690
|
+
label = f"Subagent requesting {tool_name}"
|
|
691
|
+
if summary:
|
|
692
|
+
label += f" — {summary}"
|
|
693
|
+
updates.append(label)
|
|
694
|
+
if block_type == "text":
|
|
695
|
+
text_val = getattr(block, "text", None) or (
|
|
696
|
+
block.get("text") if isinstance(block, Dict) else ""
|
|
697
|
+
)
|
|
698
|
+
if text_val:
|
|
699
|
+
snippet = str(text_val).strip()
|
|
700
|
+
if snippet:
|
|
701
|
+
short = snippet if len(snippet) <= 200 else snippet[:197] + "..."
|
|
702
|
+
updates.append(f"Subagent: {short}")
|
|
703
|
+
return updates
|
|
704
|
+
|
|
705
|
+
async def _run_subagent_background(
|
|
706
|
+
self,
|
|
707
|
+
record: AgentRunRecord,
|
|
708
|
+
subagent_context: QueryContext,
|
|
709
|
+
permission_checker: Any,
|
|
710
|
+
) -> None:
|
|
711
|
+
assistant_messages: List[AssistantMessage] = []
|
|
712
|
+
tool_use_count = 0
|
|
713
|
+
try:
|
|
714
|
+
async for message in query(
|
|
715
|
+
record.history, # type: ignore[arg-type]
|
|
716
|
+
record.system_prompt,
|
|
717
|
+
{},
|
|
718
|
+
subagent_context,
|
|
719
|
+
permission_checker,
|
|
720
|
+
):
|
|
721
|
+
if getattr(message, "type", "") == "progress":
|
|
722
|
+
continue
|
|
723
|
+
tool_use_count, _ = self._track_subagent_message(
|
|
724
|
+
message,
|
|
725
|
+
record.history,
|
|
726
|
+
assistant_messages,
|
|
727
|
+
tool_use_count,
|
|
728
|
+
)
|
|
729
|
+
except asyncio.CancelledError:
|
|
730
|
+
raise
|
|
731
|
+
except Exception as exc:
|
|
732
|
+
record.status = "failed"
|
|
733
|
+
record.error = str(exc)
|
|
734
|
+
finally:
|
|
735
|
+
record.duration_ms = (time.time() - record.start_time) * 1000
|
|
736
|
+
record.tool_use_count = tool_use_count
|
|
737
|
+
if record.status != "failed":
|
|
738
|
+
result_text = (
|
|
739
|
+
self._extract_text(assistant_messages[-1])
|
|
740
|
+
if assistant_messages
|
|
741
|
+
else "Agent returned no response."
|
|
742
|
+
)
|
|
743
|
+
record.result_text = result_text.strip()
|
|
744
|
+
record.status = "completed"
|
|
745
|
+
record.task = None
|
|
297
746
|
|
|
298
747
|
def _build_agent_prompt(self, agent: AgentDefinition, tools: List[Tool[Any, Any]]) -> str:
|
|
299
748
|
tool_names = ", ".join(tool.name for tool in tools if getattr(tool, "name", None))
|
|
@@ -370,7 +819,8 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
370
819
|
except (TypeError, ValueError) as exc:
|
|
371
820
|
logger.warning(
|
|
372
821
|
"[task_tool] Failed to serialize tool_use input: %s: %s",
|
|
373
|
-
type(exc).__name__,
|
|
822
|
+
type(exc).__name__,
|
|
823
|
+
exc,
|
|
374
824
|
extra={"tool_use_input": str(inp)[:200]},
|
|
375
825
|
)
|
|
376
826
|
serialized = str(inp)
|