ripperdoc 0.2.9__py3-none-any.whl → 0.3.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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +379 -51
- ripperdoc/cli/commands/__init__.py +6 -0
- ripperdoc/cli/commands/agents_cmd.py +128 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +63 -7
- ripperdoc/cli/commands/resume_cmd.py +5 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +14 -8
- ripperdoc/cli/ui/rich_ui.py +737 -47
- ripperdoc/cli/ui/spinner.py +93 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +24 -19
- ripperdoc/core/agents.py +14 -3
- ripperdoc/core/config.py +238 -6
- ripperdoc/core/default_tools.py +91 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +58 -0
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +160 -9
- ripperdoc/core/providers/openai.py +84 -28
- ripperdoc/core/query.py +489 -87
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +15 -5
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +354 -139
- ripperdoc/tools/bash_tool.py +117 -22
- ripperdoc/tools/file_edit_tool.py +228 -50
- ripperdoc/tools/file_read_tool.py +154 -3
- ripperdoc/tools/file_write_tool.py +53 -11
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +609 -0
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +539 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +216 -7
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +812 -0
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +7 -4
- ripperdoc/utils/messages.py +198 -33
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +242 -0
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +294 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
- ripperdoc-0.3.0.dist-info/RECORD +136 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -333
- ripperdoc-0.2.9.dist-info/RECORD +0 -123
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.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, Union
|
|
11
|
+
from uuid import uuid4
|
|
7
12
|
|
|
8
13
|
from pydantic import BaseModel, Field
|
|
9
14
|
|
|
@@ -20,29 +25,173 @@ 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 = Union[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
|
+
|
|
155
|
+
|
|
30
156
|
class TaskToolInput(BaseModel):
|
|
31
157
|
"""Input schema for delegating to a subagent."""
|
|
32
158
|
|
|
33
|
-
prompt: str = Field(
|
|
34
|
-
|
|
159
|
+
prompt: Optional[str] = Field(
|
|
160
|
+
default=None,
|
|
161
|
+
description="Detailed task description for the subagent to perform.",
|
|
162
|
+
)
|
|
163
|
+
subagent_type: Optional[str] = Field(
|
|
164
|
+
default=None,
|
|
165
|
+
description="Agent type to run (matches agent frontmatter name). Required for new runs.",
|
|
166
|
+
)
|
|
167
|
+
run_in_background: bool = Field(
|
|
168
|
+
default=False,
|
|
169
|
+
description="If true, start the agent in the background and return immediately.",
|
|
170
|
+
)
|
|
171
|
+
resume: Optional[str] = Field(
|
|
172
|
+
default=None,
|
|
173
|
+
description="Agent id to resume or fetch results for a background run.",
|
|
174
|
+
)
|
|
175
|
+
wait: bool = Field(
|
|
176
|
+
default=True,
|
|
177
|
+
description="When resuming a background agent, wait for completion before returning.",
|
|
178
|
+
)
|
|
35
179
|
|
|
36
180
|
|
|
37
181
|
class TaskToolOutput(BaseModel):
|
|
38
182
|
"""Summary of a completed subagent run."""
|
|
39
183
|
|
|
184
|
+
agent_id: Optional[str] = None
|
|
40
185
|
agent_type: str
|
|
41
186
|
result_text: str
|
|
42
187
|
duration_ms: float
|
|
43
188
|
tool_use_count: int
|
|
44
189
|
missing_tools: List[str] = Field(default_factory=list)
|
|
45
190
|
model_used: Optional[str] = None
|
|
191
|
+
status: str = "completed"
|
|
192
|
+
is_background: bool = False
|
|
193
|
+
is_resumed: bool = False
|
|
194
|
+
error: Optional[str] = None
|
|
46
195
|
|
|
47
196
|
|
|
48
197
|
class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
@@ -101,7 +250,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
101
250
|
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
251
|
f"Available agent types and the tools they have access to:\n"
|
|
103
252
|
f"{agent_block}\n\n"
|
|
104
|
-
f"When
|
|
253
|
+
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
254
|
f"When NOT to use the {task_tool_name} tool:\n"
|
|
106
255
|
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
256
|
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 +261,11 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
112
261
|
"Usage notes:\n"
|
|
113
262
|
"- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n"
|
|
114
263
|
"- 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
|
-
"-
|
|
264
|
+
f"- Use run_in_background=true to launch an agent asynchronously. The tool will return an agent_id immediately for later retrieval.\n"
|
|
265
|
+
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"
|
|
266
|
+
"- To continue a completed agent, call Task with resume=<agent_id> and a new prompt.\n"
|
|
118
267
|
"- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n"
|
|
119
|
-
|
|
268
|
+
"- 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
269
|
"- The agent's outputs should generally be trusted\n"
|
|
121
270
|
"- 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
271
|
"- 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 +274,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
125
274
|
"Example usage:\n"
|
|
126
275
|
"\n"
|
|
127
276
|
"<example_agent_descriptions>\n"
|
|
128
|
-
'"code-reviewer": use this agent after you are done writing a
|
|
277
|
+
'"code-reviewer": use this agent after you are done writing a significant piece of code\n'
|
|
129
278
|
'"greeting-responder": use this agent when to respond to user greetings with a friendly joke\n'
|
|
130
279
|
"</example_agent_description>\n"
|
|
131
280
|
"\n"
|
|
@@ -144,7 +293,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
144
293
|
"}\n"
|
|
145
294
|
"</code>\n"
|
|
146
295
|
"<commentary>\n"
|
|
147
|
-
"Since a
|
|
296
|
+
"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
297
|
"</commentary>\n"
|
|
149
298
|
"assistant: Now let me use the code-reviewer agent to review the code\n"
|
|
150
299
|
f"assistant: Uses the {task_tool_name} tool to launch the code-reviewer agent \n"
|
|
@@ -165,20 +314,60 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
165
314
|
def is_concurrency_safe(self) -> bool:
|
|
166
315
|
return True
|
|
167
316
|
|
|
317
|
+
async def validate_input(
|
|
318
|
+
self, input_data: TaskToolInput, context: Optional[ToolUseContext] = None
|
|
319
|
+
) -> ValidationResult:
|
|
320
|
+
del context
|
|
321
|
+
if input_data.resume and input_data.run_in_background:
|
|
322
|
+
return ValidationResult(
|
|
323
|
+
result=False,
|
|
324
|
+
message="run_in_background cannot be used when resuming an agent.",
|
|
325
|
+
)
|
|
326
|
+
if input_data.resume:
|
|
327
|
+
if input_data.prompt is not None and not input_data.prompt.strip():
|
|
328
|
+
return ValidationResult(
|
|
329
|
+
result=False,
|
|
330
|
+
message="prompt cannot be empty when resuming with new work.",
|
|
331
|
+
)
|
|
332
|
+
return ValidationResult(result=True)
|
|
333
|
+
|
|
334
|
+
if not input_data.subagent_type:
|
|
335
|
+
return ValidationResult(
|
|
336
|
+
result=False,
|
|
337
|
+
message="subagent_type is required when starting a new agent.",
|
|
338
|
+
)
|
|
339
|
+
if not input_data.prompt or not input_data.prompt.strip():
|
|
340
|
+
return ValidationResult(
|
|
341
|
+
result=False,
|
|
342
|
+
message="prompt is required when starting a new agent.",
|
|
343
|
+
)
|
|
344
|
+
return ValidationResult(result=True)
|
|
345
|
+
|
|
168
346
|
def render_result_for_assistant(self, output: TaskToolOutput) -> str:
|
|
169
347
|
details: List[str] = []
|
|
348
|
+
if output.agent_id:
|
|
349
|
+
details.append(f"id={output.agent_id}")
|
|
350
|
+
if output.status and output.status != "completed":
|
|
351
|
+
details.append(output.status)
|
|
170
352
|
if output.tool_use_count:
|
|
171
353
|
details.append(f"{output.tool_use_count} tool uses")
|
|
172
354
|
details.append(f"{output.duration_ms / 1000:.1f}s")
|
|
173
355
|
if output.missing_tools:
|
|
174
356
|
details.append(f"missing tools: {', '.join(output.missing_tools)}")
|
|
357
|
+
if output.error:
|
|
358
|
+
details.append(f"error: {output.error}")
|
|
175
359
|
|
|
176
360
|
suffix = f" ({'; '.join(details)})" if details else ""
|
|
177
361
|
return f"[subagent:{output.agent_type}] {output.result_text}{suffix}"
|
|
178
362
|
|
|
179
363
|
def render_tool_use_message(self, input_data: TaskToolInput, verbose: bool = False) -> str:
|
|
180
364
|
del verbose
|
|
181
|
-
|
|
365
|
+
if input_data.resume:
|
|
366
|
+
return f"Resume subagent {input_data.resume}"
|
|
367
|
+
label = f"Task via {input_data.subagent_type}: {input_data.prompt}"
|
|
368
|
+
if input_data.run_in_background:
|
|
369
|
+
label += " (background)"
|
|
370
|
+
return label
|
|
182
371
|
|
|
183
372
|
async def call(
|
|
184
373
|
self,
|
|
@@ -187,6 +376,123 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
187
376
|
) -> AsyncGenerator[ToolOutput, None]:
|
|
188
377
|
clear_agent_cache()
|
|
189
378
|
agents = load_agent_definitions()
|
|
379
|
+
|
|
380
|
+
if input_data.resume:
|
|
381
|
+
record = _get_agent_run(input_data.resume)
|
|
382
|
+
if not record:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"Agent id '{input_data.resume}' not found. "
|
|
385
|
+
"Start a new agent to obtain a valid agent_id."
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if record.task and not record.task.done():
|
|
389
|
+
if not input_data.wait:
|
|
390
|
+
output = self._output_from_record(
|
|
391
|
+
record,
|
|
392
|
+
status_override="running",
|
|
393
|
+
result_text_override="Agent is still running in the background.",
|
|
394
|
+
is_background=True,
|
|
395
|
+
is_resumed=True,
|
|
396
|
+
)
|
|
397
|
+
yield ToolResult(
|
|
398
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
399
|
+
)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
yield ToolProgress(
|
|
403
|
+
content=f"Waiting for subagent '{record.agent_type}' ({record.agent_id})"
|
|
404
|
+
)
|
|
405
|
+
try:
|
|
406
|
+
await record.task
|
|
407
|
+
except asyncio.CancelledError:
|
|
408
|
+
raise
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
record.status = "failed"
|
|
411
|
+
record.error = str(exc)
|
|
412
|
+
|
|
413
|
+
if not input_data.prompt:
|
|
414
|
+
output = self._output_from_record(
|
|
415
|
+
record,
|
|
416
|
+
is_background=bool(record.task),
|
|
417
|
+
is_resumed=True,
|
|
418
|
+
)
|
|
419
|
+
yield ToolResult(
|
|
420
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
421
|
+
)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
record.history.append(create_user_message(input_data.prompt))
|
|
425
|
+
record.start_time = time.time()
|
|
426
|
+
record.duration_ms = 0.0
|
|
427
|
+
record.tool_use_count = 0
|
|
428
|
+
record.status = "running"
|
|
429
|
+
record.result_text = None
|
|
430
|
+
record.error = None
|
|
431
|
+
record.task = None
|
|
432
|
+
|
|
433
|
+
subagent_context = QueryContext(
|
|
434
|
+
tools=record.tools,
|
|
435
|
+
yolo_mode=context.yolo_mode,
|
|
436
|
+
verbose=context.verbose,
|
|
437
|
+
model=record.model_used or "main",
|
|
438
|
+
stop_hook="subagent",
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
yield ToolProgress(content=f"Resuming subagent '{record.agent_type}'")
|
|
442
|
+
|
|
443
|
+
# Get the Task tool's tool_use_id to set as parent_tool_use_id for subagent messages
|
|
444
|
+
parent_tool_use_id = context.message_id
|
|
445
|
+
|
|
446
|
+
assistant_messages: List[AssistantMessage] = []
|
|
447
|
+
tool_use_count = 0
|
|
448
|
+
async for message in query(
|
|
449
|
+
record.history, # type: ignore[arg-type]
|
|
450
|
+
record.system_prompt,
|
|
451
|
+
{},
|
|
452
|
+
subagent_context,
|
|
453
|
+
context.permission_checker,
|
|
454
|
+
):
|
|
455
|
+
msg_type = getattr(message, "type", "")
|
|
456
|
+
if msg_type == "progress":
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Track the message for internal state
|
|
460
|
+
tool_use_count, updates = self._track_subagent_message(
|
|
461
|
+
message,
|
|
462
|
+
record.history,
|
|
463
|
+
assistant_messages,
|
|
464
|
+
tool_use_count,
|
|
465
|
+
)
|
|
466
|
+
for update in updates:
|
|
467
|
+
yield ToolProgress(content=update)
|
|
468
|
+
|
|
469
|
+
# CRITICAL: Also yield subagent messages to SDK for compatibility
|
|
470
|
+
if msg_type in ("assistant", "user"):
|
|
471
|
+
# Set parent_tool_use_id to link subagent messages to the Task tool call
|
|
472
|
+
message_with_parent = message.model_copy(update={"parent_tool_use_id": parent_tool_use_id})
|
|
473
|
+
yield ToolProgress(content=message_with_parent, is_subagent_message=True)
|
|
474
|
+
|
|
475
|
+
duration_ms = (time.time() - record.start_time) * 1000
|
|
476
|
+
result_text = (
|
|
477
|
+
self._extract_text(assistant_messages[-1])
|
|
478
|
+
if assistant_messages
|
|
479
|
+
else "Agent returned no response."
|
|
480
|
+
)
|
|
481
|
+
record.duration_ms = duration_ms
|
|
482
|
+
record.tool_use_count = tool_use_count
|
|
483
|
+
record.result_text = result_text.strip()
|
|
484
|
+
record.status = "completed"
|
|
485
|
+
|
|
486
|
+
output = self._output_from_record(
|
|
487
|
+
record,
|
|
488
|
+
result_text_override=result_text.strip(),
|
|
489
|
+
is_resumed=True,
|
|
490
|
+
)
|
|
491
|
+
yield ToolResult(
|
|
492
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
493
|
+
)
|
|
494
|
+
return
|
|
495
|
+
|
|
190
496
|
target_agent = next(
|
|
191
497
|
(
|
|
192
498
|
agent
|
|
@@ -217,83 +523,251 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
|
|
|
217
523
|
]
|
|
218
524
|
|
|
219
525
|
agent_system_prompt = self._build_agent_prompt(target_agent, typed_agent_tools)
|
|
220
|
-
|
|
526
|
+
parent_history = (
|
|
527
|
+
self._coerce_parent_history(getattr(context, "conversation_messages", None))
|
|
528
|
+
if target_agent.fork_context
|
|
529
|
+
else []
|
|
530
|
+
)
|
|
531
|
+
subagent_messages = [
|
|
532
|
+
*parent_history,
|
|
533
|
+
create_user_message(input_data.prompt or ""),
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
agent_id = _new_agent_id()
|
|
537
|
+
record = AgentRunRecord(
|
|
538
|
+
agent_id=agent_id,
|
|
539
|
+
agent_type=target_agent.agent_type,
|
|
540
|
+
tools=typed_agent_tools,
|
|
541
|
+
system_prompt=agent_system_prompt,
|
|
542
|
+
history=subagent_messages,
|
|
543
|
+
missing_tools=missing_tools,
|
|
544
|
+
model_used=target_agent.model or "main",
|
|
545
|
+
start_time=time.time(),
|
|
546
|
+
is_background=bool(input_data.run_in_background),
|
|
547
|
+
)
|
|
548
|
+
_register_agent_run(record)
|
|
221
549
|
|
|
222
550
|
subagent_context = QueryContext(
|
|
223
551
|
tools=typed_agent_tools,
|
|
224
552
|
yolo_mode=context.yolo_mode,
|
|
225
553
|
verbose=context.verbose,
|
|
226
|
-
model=target_agent.model or "
|
|
554
|
+
model=target_agent.model or "main",
|
|
555
|
+
stop_hook="subagent",
|
|
227
556
|
)
|
|
228
557
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
558
|
+
if input_data.run_in_background:
|
|
559
|
+
record.task = asyncio.create_task(
|
|
560
|
+
self._run_subagent_background(
|
|
561
|
+
record,
|
|
562
|
+
subagent_context,
|
|
563
|
+
context.permission_checker,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
output = self._output_from_record(
|
|
567
|
+
record,
|
|
568
|
+
status_override="running",
|
|
569
|
+
result_text_override="Agent started in the background.",
|
|
570
|
+
is_background=True,
|
|
571
|
+
)
|
|
572
|
+
yield ToolResult(
|
|
573
|
+
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
574
|
+
)
|
|
575
|
+
return
|
|
232
576
|
|
|
233
577
|
yield ToolProgress(content=f"Launching subagent '{target_agent.agent_type}'")
|
|
234
578
|
|
|
579
|
+
# Get the Task tool's tool_use_id to set as parent_tool_use_id for subagent messages
|
|
580
|
+
parent_tool_use_id = context.message_id
|
|
581
|
+
|
|
582
|
+
assistant_messages = []
|
|
583
|
+
tool_use_count = 0
|
|
235
584
|
async for message in query(
|
|
236
|
-
|
|
585
|
+
record.history, # type: ignore[arg-type]
|
|
237
586
|
agent_system_prompt,
|
|
238
587
|
{},
|
|
239
588
|
subagent_context,
|
|
240
589
|
context.permission_checker,
|
|
241
590
|
):
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
591
|
+
msg_type = getattr(message, "type", "")
|
|
592
|
+
if msg_type == "progress":
|
|
593
|
+
continue
|
|
594
|
+
|
|
595
|
+
# Track the message for internal state
|
|
596
|
+
tool_use_count, updates = self._track_subagent_message(
|
|
597
|
+
message,
|
|
598
|
+
record.history,
|
|
599
|
+
assistant_messages,
|
|
600
|
+
tool_use_count,
|
|
601
|
+
)
|
|
602
|
+
for update in updates:
|
|
603
|
+
yield ToolProgress(content=update)
|
|
604
|
+
|
|
605
|
+
# CRITICAL: Also yield subagent messages to SDK for compatibility
|
|
606
|
+
# This allows SDK clients to see the full subagent conversation
|
|
607
|
+
if msg_type in ("assistant", "user"):
|
|
608
|
+
# Set parent_tool_use_id to link subagent messages to the Task tool call
|
|
609
|
+
# Use model_copy() to create a new message with the parent_tool_use_id set
|
|
610
|
+
message_with_parent = message.model_copy(update={"parent_tool_use_id": parent_tool_use_id})
|
|
611
|
+
yield ToolProgress(content=message_with_parent, is_subagent_message=True)
|
|
612
|
+
|
|
613
|
+
duration_ms = (time.time() - record.start_time) * 1000
|
|
281
614
|
result_text = (
|
|
282
615
|
self._extract_text(assistant_messages[-1])
|
|
283
616
|
if assistant_messages
|
|
284
617
|
else "Agent returned no response."
|
|
285
618
|
)
|
|
286
619
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
620
|
+
record.duration_ms = duration_ms
|
|
621
|
+
record.tool_use_count = tool_use_count
|
|
622
|
+
record.result_text = result_text.strip()
|
|
623
|
+
record.status = "completed"
|
|
624
|
+
|
|
625
|
+
output = self._output_from_record(record, result_text_override=result_text.strip())
|
|
626
|
+
|
|
627
|
+
yield ToolResult(data=output, result_for_assistant=self.render_result_for_assistant(output))
|
|
628
|
+
|
|
629
|
+
def _output_from_record(
|
|
630
|
+
self,
|
|
631
|
+
record: AgentRunRecord,
|
|
632
|
+
*,
|
|
633
|
+
status_override: Optional[str] = None,
|
|
634
|
+
result_text_override: Optional[str] = None,
|
|
635
|
+
is_background: bool = False,
|
|
636
|
+
is_resumed: bool = False,
|
|
637
|
+
error_override: Optional[str] = None,
|
|
638
|
+
) -> TaskToolOutput:
|
|
639
|
+
status = status_override or record.status
|
|
640
|
+
duration_ms = (
|
|
641
|
+
record.duration_ms
|
|
642
|
+
if record.duration_ms
|
|
643
|
+
else max((time.time() - record.start_time) * 1000, 0.0)
|
|
644
|
+
)
|
|
645
|
+
result_text = (
|
|
646
|
+
result_text_override
|
|
647
|
+
or record.result_text
|
|
648
|
+
or ("Agent is still running." if status == "running" else "Agent returned no response.")
|
|
649
|
+
)
|
|
650
|
+
return TaskToolOutput(
|
|
651
|
+
agent_id=record.agent_id,
|
|
652
|
+
agent_type=record.agent_type,
|
|
653
|
+
result_text=result_text,
|
|
290
654
|
duration_ms=duration_ms,
|
|
291
|
-
tool_use_count=tool_use_count,
|
|
292
|
-
missing_tools=missing_tools,
|
|
293
|
-
model_used=
|
|
655
|
+
tool_use_count=record.tool_use_count,
|
|
656
|
+
missing_tools=record.missing_tools,
|
|
657
|
+
model_used=record.model_used,
|
|
658
|
+
status=status,
|
|
659
|
+
is_background=is_background,
|
|
660
|
+
is_resumed=is_resumed,
|
|
661
|
+
error=error_override or record.error,
|
|
294
662
|
)
|
|
295
663
|
|
|
296
|
-
|
|
664
|
+
def _coerce_parent_history(self, messages: Optional[Sequence[object]]) -> List[MessageType]:
|
|
665
|
+
if not messages:
|
|
666
|
+
return []
|
|
667
|
+
history: List[MessageType] = []
|
|
668
|
+
for msg in messages:
|
|
669
|
+
msg_type = getattr(msg, "type", None)
|
|
670
|
+
if msg_type in ("user", "assistant"):
|
|
671
|
+
history.append(msg) # type: ignore[arg-type]
|
|
672
|
+
return history
|
|
673
|
+
|
|
674
|
+
def _track_subagent_message(
|
|
675
|
+
self,
|
|
676
|
+
message: object,
|
|
677
|
+
history: List[MessageType],
|
|
678
|
+
assistant_messages: List[AssistantMessage],
|
|
679
|
+
tool_use_count: int,
|
|
680
|
+
) -> tuple[int, List[str]]:
|
|
681
|
+
updates: List[str] = []
|
|
682
|
+
msg_type = getattr(message, "type", "")
|
|
683
|
+
if msg_type in ("assistant", "user"):
|
|
684
|
+
history.append(message) # type: ignore[arg-type]
|
|
685
|
+
|
|
686
|
+
if msg_type == "assistant":
|
|
687
|
+
if isinstance(message, AssistantMessage):
|
|
688
|
+
tool_use_count += self._count_tool_uses(message)
|
|
689
|
+
updates = self._extract_progress_updates(message)
|
|
690
|
+
assistant_messages.append(message) # type: ignore[arg-type]
|
|
691
|
+
|
|
692
|
+
return tool_use_count, updates
|
|
693
|
+
|
|
694
|
+
def _extract_progress_updates(self, message: object) -> List[str]:
|
|
695
|
+
msg_content = getattr(message, "message", None)
|
|
696
|
+
blocks = getattr(msg_content, "content", []) if msg_content else []
|
|
697
|
+
if not isinstance(blocks, list):
|
|
698
|
+
return []
|
|
699
|
+
|
|
700
|
+
updates: List[str] = []
|
|
701
|
+
for block in blocks:
|
|
702
|
+
block_type = getattr(block, "type", None) or (
|
|
703
|
+
block.get("type") if isinstance(block, Dict) else None
|
|
704
|
+
)
|
|
705
|
+
if block_type == "tool_use":
|
|
706
|
+
tool_name = getattr(block, "name", None) or (
|
|
707
|
+
block.get("name") if isinstance(block, Dict) else "unknown tool"
|
|
708
|
+
)
|
|
709
|
+
block_input = (
|
|
710
|
+
getattr(block, "input", None)
|
|
711
|
+
if hasattr(block, "input")
|
|
712
|
+
else (block.get("input") if isinstance(block, Dict) else None)
|
|
713
|
+
)
|
|
714
|
+
summary = self._summarize_tool_input(block_input)
|
|
715
|
+
label = f"Subagent requesting {tool_name}"
|
|
716
|
+
if summary:
|
|
717
|
+
label += f" — {summary}"
|
|
718
|
+
updates.append(label)
|
|
719
|
+
if block_type == "text":
|
|
720
|
+
text_val = getattr(block, "text", None) or (
|
|
721
|
+
block.get("text") if isinstance(block, Dict) else ""
|
|
722
|
+
)
|
|
723
|
+
if text_val:
|
|
724
|
+
snippet = str(text_val).strip()
|
|
725
|
+
if snippet:
|
|
726
|
+
short = snippet if len(snippet) <= 200 else snippet[:197] + "..."
|
|
727
|
+
updates.append(f"Subagent: {short}")
|
|
728
|
+
return updates
|
|
729
|
+
|
|
730
|
+
async def _run_subagent_background(
|
|
731
|
+
self,
|
|
732
|
+
record: AgentRunRecord,
|
|
733
|
+
subagent_context: QueryContext,
|
|
734
|
+
permission_checker: Any,
|
|
735
|
+
) -> None:
|
|
736
|
+
assistant_messages: List[AssistantMessage] = []
|
|
737
|
+
tool_use_count = 0
|
|
738
|
+
try:
|
|
739
|
+
async for message in query(
|
|
740
|
+
record.history, # type: ignore[arg-type]
|
|
741
|
+
record.system_prompt,
|
|
742
|
+
{},
|
|
743
|
+
subagent_context,
|
|
744
|
+
permission_checker,
|
|
745
|
+
):
|
|
746
|
+
if getattr(message, "type", "") == "progress":
|
|
747
|
+
continue
|
|
748
|
+
tool_use_count, _ = self._track_subagent_message(
|
|
749
|
+
message,
|
|
750
|
+
record.history,
|
|
751
|
+
assistant_messages,
|
|
752
|
+
tool_use_count,
|
|
753
|
+
)
|
|
754
|
+
except asyncio.CancelledError:
|
|
755
|
+
raise
|
|
756
|
+
except Exception as exc:
|
|
757
|
+
record.status = "failed"
|
|
758
|
+
record.error = str(exc)
|
|
759
|
+
finally:
|
|
760
|
+
record.duration_ms = (time.time() - record.start_time) * 1000
|
|
761
|
+
record.tool_use_count = tool_use_count
|
|
762
|
+
if record.status != "failed":
|
|
763
|
+
result_text = (
|
|
764
|
+
self._extract_text(assistant_messages[-1])
|
|
765
|
+
if assistant_messages
|
|
766
|
+
else "Agent returned no response."
|
|
767
|
+
)
|
|
768
|
+
record.result_text = result_text.strip()
|
|
769
|
+
record.status = "completed"
|
|
770
|
+
record.task = None
|
|
297
771
|
|
|
298
772
|
def _build_agent_prompt(self, agent: AgentDefinition, tools: List[Tool[Any, Any]]) -> str:
|
|
299
773
|
tool_names = ", ".join(tool.name for tool in tools if getattr(tool, "name", None))
|