tunacode-cli 0.1.21__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Helper functions for agent operations to reduce code duplication."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from tunacode.types import FallbackResponse, StateManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserPromptPartFallback:
|
|
9
|
+
"""Fallback class for UserPromptPart when pydantic_ai is not available."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, content: str, part_kind: str):
|
|
12
|
+
self.content = content
|
|
13
|
+
self.part_kind = part_kind
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Cache for UserPromptPart class
|
|
17
|
+
_USER_PROMPT_PART_CLASS = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_user_prompt_part_class():
|
|
21
|
+
"""Get UserPromptPart class with caching and fallback for test environment."""
|
|
22
|
+
global _USER_PROMPT_PART_CLASS
|
|
23
|
+
|
|
24
|
+
if _USER_PROMPT_PART_CLASS is not None:
|
|
25
|
+
return _USER_PROMPT_PART_CLASS
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import importlib
|
|
29
|
+
|
|
30
|
+
messages = importlib.import_module("pydantic_ai.messages")
|
|
31
|
+
_USER_PROMPT_PART_CLASS = getattr(messages, "UserPromptPart", None)
|
|
32
|
+
|
|
33
|
+
if _USER_PROMPT_PART_CLASS is None:
|
|
34
|
+
_USER_PROMPT_PART_CLASS = UserPromptPartFallback
|
|
35
|
+
except Exception:
|
|
36
|
+
_USER_PROMPT_PART_CLASS = UserPromptPartFallback
|
|
37
|
+
|
|
38
|
+
return _USER_PROMPT_PART_CLASS
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_user_message(content: str, state_manager: StateManager):
|
|
42
|
+
"""Create a user message and add it to the session messages."""
|
|
43
|
+
from .message_handler import get_model_messages
|
|
44
|
+
|
|
45
|
+
model_request_cls = get_model_messages()[0]
|
|
46
|
+
UserPromptPart = get_user_prompt_part_class()
|
|
47
|
+
user_prompt_part = UserPromptPart(content=content, part_kind="user-prompt")
|
|
48
|
+
message = model_request_cls(parts=[user_prompt_part], kind="request")
|
|
49
|
+
state_manager.session.messages.append(message)
|
|
50
|
+
state_manager.session.update_token_count()
|
|
51
|
+
return message
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_tool_summary(tool_calls: list[dict[str, Any]]) -> dict[str, int]:
|
|
55
|
+
"""Generate a summary of tool usage from tool calls."""
|
|
56
|
+
tool_summary: dict[str, int] = {}
|
|
57
|
+
for tc in tool_calls:
|
|
58
|
+
tool_name = tc.get("tool", "unknown")
|
|
59
|
+
tool_summary[tool_name] = tool_summary.get(tool_name, 0) + 1
|
|
60
|
+
return tool_summary
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_tool_description(tool_name: str, tool_args: dict[str, Any]) -> str:
|
|
64
|
+
"""Get a descriptive string for a tool call."""
|
|
65
|
+
tool_desc = tool_name
|
|
66
|
+
if tool_name in ["grep", "glob"] and isinstance(tool_args, dict):
|
|
67
|
+
pattern = tool_args.get("pattern", "")
|
|
68
|
+
tool_desc = f"{tool_name}('{pattern}')"
|
|
69
|
+
elif tool_name == "read_file" and isinstance(tool_args, dict):
|
|
70
|
+
path = tool_args.get("file_path", tool_args.get("filepath", ""))
|
|
71
|
+
tool_desc = f"{tool_name}('{path}')"
|
|
72
|
+
return tool_desc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_readable_tool_description(tool_name: str, tool_args: dict[str, Any]) -> str:
|
|
76
|
+
"""Get a human-readable description of a tool operation for batch panel display."""
|
|
77
|
+
if not isinstance(tool_args, dict):
|
|
78
|
+
return f"Executing `{tool_name}`"
|
|
79
|
+
|
|
80
|
+
if tool_name == "read_file":
|
|
81
|
+
file_path = tool_args.get("file_path", tool_args.get("filepath", ""))
|
|
82
|
+
if file_path:
|
|
83
|
+
return f"Reading `{file_path}`"
|
|
84
|
+
return "Reading file"
|
|
85
|
+
|
|
86
|
+
if tool_name == "list_dir":
|
|
87
|
+
directory = tool_args.get("directory", "")
|
|
88
|
+
if directory:
|
|
89
|
+
return f"Listing directory `{directory}`"
|
|
90
|
+
return "Listing directory"
|
|
91
|
+
|
|
92
|
+
if tool_name == "grep":
|
|
93
|
+
pattern = tool_args.get("pattern", "")
|
|
94
|
+
include_files = tool_args.get("include_files", "")
|
|
95
|
+
if pattern and include_files:
|
|
96
|
+
return f"Searching for `{pattern}` in `{include_files}`"
|
|
97
|
+
if pattern:
|
|
98
|
+
return f"Searching for `{pattern}`"
|
|
99
|
+
return "Searching files"
|
|
100
|
+
|
|
101
|
+
if tool_name == "glob":
|
|
102
|
+
pattern = tool_args.get("pattern", "")
|
|
103
|
+
if pattern:
|
|
104
|
+
return f"Finding files matching `{pattern}`"
|
|
105
|
+
return "Finding files"
|
|
106
|
+
|
|
107
|
+
if tool_name == "research_codebase":
|
|
108
|
+
query = tool_args.get("query", "")
|
|
109
|
+
if query:
|
|
110
|
+
# Truncate long queries for display
|
|
111
|
+
query_display = query[:60] + "..." if len(query) > 60 else query
|
|
112
|
+
return f"Researching: {query_display}"
|
|
113
|
+
return "Researching codebase"
|
|
114
|
+
|
|
115
|
+
# Fallback for unknown tools
|
|
116
|
+
return f"Executing `{tool_name}`"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_recent_tools_context(tool_calls: list[dict[str, Any]], limit: int = 3) -> str:
|
|
120
|
+
"""Get a context string describing recent tool usage."""
|
|
121
|
+
if not tool_calls:
|
|
122
|
+
return "No tools used yet"
|
|
123
|
+
|
|
124
|
+
last_tools = []
|
|
125
|
+
for tc in tool_calls[-limit:]:
|
|
126
|
+
tool_name = tc.get("tool", "unknown")
|
|
127
|
+
tool_args = tc.get("args", {})
|
|
128
|
+
tool_desc = get_tool_description(tool_name, tool_args)
|
|
129
|
+
last_tools.append(tool_desc)
|
|
130
|
+
|
|
131
|
+
return f"Recent tools: {', '.join(last_tools)}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create_empty_response_message(
|
|
135
|
+
message: str,
|
|
136
|
+
empty_reason: str,
|
|
137
|
+
tool_calls: list[dict[str, Any]],
|
|
138
|
+
iteration: int,
|
|
139
|
+
state_manager: StateManager,
|
|
140
|
+
) -> str:
|
|
141
|
+
"""Create a constructive message for handling empty responses."""
|
|
142
|
+
tools_context = get_recent_tools_context(tool_calls)
|
|
143
|
+
|
|
144
|
+
reason = empty_reason if empty_reason != "empty" else "empty"
|
|
145
|
+
content = f"""Response appears {reason} or incomplete. Let's troubleshoot and try again.
|
|
146
|
+
|
|
147
|
+
Task: {message[:200]}...
|
|
148
|
+
{tools_context}
|
|
149
|
+
Attempt: {iteration}
|
|
150
|
+
|
|
151
|
+
Please take one of these specific actions:
|
|
152
|
+
|
|
153
|
+
1. **Search yielded no results?** → Try alternative search terms or broader patterns
|
|
154
|
+
2. **Found what you need?** → Use TUNACODE DONE: to finalize
|
|
155
|
+
3. **Encountering a blocker?** → Explain the specific issue preventing progress
|
|
156
|
+
4. **Need more context?** → Use list_dir or expand your search scope
|
|
157
|
+
|
|
158
|
+
**Expected in your response:**
|
|
159
|
+
- Execute at least one tool OR provide substantial analysis
|
|
160
|
+
- If stuck, clearly describe what you've tried and what's blocking you
|
|
161
|
+
- Avoid empty responses - the system needs actionable output to proceed
|
|
162
|
+
|
|
163
|
+
Ready to continue with a complete response."""
|
|
164
|
+
|
|
165
|
+
return content
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_progress_summary(tool_calls: list[dict[str, Any]]) -> tuple[dict[str, int], str]:
|
|
169
|
+
"""Create a progress summary from tool calls."""
|
|
170
|
+
tool_summary = get_tool_summary(tool_calls)
|
|
171
|
+
|
|
172
|
+
if tool_summary:
|
|
173
|
+
summary_str = ", ".join([f"{name}: {count}" for name, count in tool_summary.items()])
|
|
174
|
+
else:
|
|
175
|
+
summary_str = "No tools used yet"
|
|
176
|
+
|
|
177
|
+
return tool_summary, summary_str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_fallback_response(
|
|
181
|
+
iterations: int,
|
|
182
|
+
max_iterations: int,
|
|
183
|
+
tool_calls: list[dict[str, Any]],
|
|
184
|
+
messages: list[Any],
|
|
185
|
+
verbosity: str = "normal",
|
|
186
|
+
) -> FallbackResponse:
|
|
187
|
+
"""Create a comprehensive fallback response when iteration limit is reached."""
|
|
188
|
+
fallback = FallbackResponse(
|
|
189
|
+
summary="Reached maximum iterations without producing a final response.",
|
|
190
|
+
progress=f"Completed {iterations} iterations (limit: {max_iterations})",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Extract context from messages
|
|
194
|
+
tool_calls_summary = []
|
|
195
|
+
files_modified = set()
|
|
196
|
+
commands_run = []
|
|
197
|
+
|
|
198
|
+
for msg in messages:
|
|
199
|
+
if hasattr(msg, "parts"):
|
|
200
|
+
for part in msg.parts:
|
|
201
|
+
if hasattr(part, "part_kind") and part.part_kind == "tool-call":
|
|
202
|
+
tool_name = getattr(part, "tool_name", "unknown")
|
|
203
|
+
tool_calls_summary.append(tool_name)
|
|
204
|
+
|
|
205
|
+
# Track specific operations
|
|
206
|
+
if (
|
|
207
|
+
tool_name in ["write_file", "update_file"]
|
|
208
|
+
and hasattr(part, "args")
|
|
209
|
+
and isinstance(part.args, dict)
|
|
210
|
+
and "file_path" in part.args
|
|
211
|
+
):
|
|
212
|
+
files_modified.add(part.args["file_path"])
|
|
213
|
+
elif (
|
|
214
|
+
tool_name == "bash"
|
|
215
|
+
and hasattr(part, "args")
|
|
216
|
+
and isinstance(part.args, dict)
|
|
217
|
+
and "command" in part.args
|
|
218
|
+
):
|
|
219
|
+
commands_run.append(part.args["command"])
|
|
220
|
+
|
|
221
|
+
if verbosity in ["normal", "detailed"]:
|
|
222
|
+
# Add what was attempted
|
|
223
|
+
if tool_calls_summary:
|
|
224
|
+
tool_counts: dict[str, int] = {}
|
|
225
|
+
for tool in tool_calls_summary:
|
|
226
|
+
tool_counts[tool] = tool_counts.get(tool, 0) + 1
|
|
227
|
+
|
|
228
|
+
fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
|
|
229
|
+
for tool, count in sorted(tool_counts.items()):
|
|
230
|
+
fallback.issues.append(f" • {tool}: {count}x")
|
|
231
|
+
|
|
232
|
+
if verbosity == "detailed":
|
|
233
|
+
if files_modified:
|
|
234
|
+
fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
|
|
235
|
+
for f in sorted(files_modified)[:5]:
|
|
236
|
+
fallback.issues.append(f" • {f}")
|
|
237
|
+
if len(files_modified) > 5:
|
|
238
|
+
fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
|
|
239
|
+
|
|
240
|
+
if commands_run:
|
|
241
|
+
fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
|
|
242
|
+
for cmd in commands_run[:3]:
|
|
243
|
+
display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
|
|
244
|
+
fallback.issues.append(f" • {display_cmd}")
|
|
245
|
+
if len(commands_run) > 3:
|
|
246
|
+
fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
|
|
247
|
+
|
|
248
|
+
# Add helpful next steps
|
|
249
|
+
fallback.next_steps.append("The task may be too complex - try breaking it into smaller steps")
|
|
250
|
+
fallback.next_steps.append("Check the output above for any errors or partial progress")
|
|
251
|
+
if files_modified:
|
|
252
|
+
fallback.next_steps.append("Review modified files to see what changes were made")
|
|
253
|
+
|
|
254
|
+
return fallback
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def handle_empty_response(
|
|
258
|
+
message: str,
|
|
259
|
+
reason: str,
|
|
260
|
+
iter_index: int,
|
|
261
|
+
state: Any,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Handle empty responses by creating a synthetic user message with retry guidance."""
|
|
264
|
+
force_action_content = create_empty_response_message(
|
|
265
|
+
message,
|
|
266
|
+
reason,
|
|
267
|
+
getattr(state.sm.session, "tool_calls", []),
|
|
268
|
+
iter_index,
|
|
269
|
+
state.sm,
|
|
270
|
+
)
|
|
271
|
+
create_user_message(force_action_content, state.sm)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def format_fallback_output(fallback: FallbackResponse) -> str:
|
|
275
|
+
"""Format a fallback response into a comprehensive output string."""
|
|
276
|
+
output_parts = [fallback.summary, ""]
|
|
277
|
+
|
|
278
|
+
if fallback.progress:
|
|
279
|
+
output_parts.append(f"Progress: {fallback.progress}")
|
|
280
|
+
|
|
281
|
+
if fallback.issues:
|
|
282
|
+
output_parts.append("\nWhat happened:")
|
|
283
|
+
output_parts.extend(fallback.issues)
|
|
284
|
+
|
|
285
|
+
if fallback.next_steps:
|
|
286
|
+
output_parts.append("\nSuggested next steps:")
|
|
287
|
+
for step in fallback.next_steps:
|
|
288
|
+
output_parts.append(f" • {step}")
|
|
289
|
+
|
|
290
|
+
return "\n".join(output_parts)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Message handling utilities for agent communication."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from tunacode.core.state import StateManager
|
|
6
|
+
|
|
7
|
+
ToolCallId = str
|
|
8
|
+
ToolName = str
|
|
9
|
+
ErrorMessage = str | None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_model_messages():
|
|
13
|
+
"""
|
|
14
|
+
Safely retrieve message-related classes from pydantic_ai.
|
|
15
|
+
|
|
16
|
+
If the running environment (e.g. our test stubs) does not define
|
|
17
|
+
SystemPromptPart we create a minimal placeholder so that the rest of the
|
|
18
|
+
code can continue to work without depending on the real implementation.
|
|
19
|
+
"""
|
|
20
|
+
import importlib
|
|
21
|
+
|
|
22
|
+
messages = importlib.import_module("pydantic_ai.messages")
|
|
23
|
+
|
|
24
|
+
# Get the required classes
|
|
25
|
+
ModelRequest = messages.ModelRequest
|
|
26
|
+
ToolReturnPart = messages.ToolReturnPart
|
|
27
|
+
|
|
28
|
+
# Create minimal fallback for SystemPromptPart if it doesn't exist
|
|
29
|
+
if not hasattr(messages, "SystemPromptPart"):
|
|
30
|
+
|
|
31
|
+
class SystemPromptPart: # type: ignore
|
|
32
|
+
def __init__(self, content: str = "", role: str = "system", part_kind: str = ""):
|
|
33
|
+
self.content = content
|
|
34
|
+
self.role = role
|
|
35
|
+
self.part_kind = part_kind
|
|
36
|
+
else:
|
|
37
|
+
SystemPromptPart = messages.SystemPromptPart
|
|
38
|
+
|
|
39
|
+
return ModelRequest, ToolReturnPart, SystemPromptPart
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def patch_tool_messages(
|
|
43
|
+
error_message: ErrorMessage = "Tool operation failed",
|
|
44
|
+
state_manager: StateManager = None,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Find any tool calls without responses and add synthetic error responses for them.
|
|
48
|
+
Takes an error message to use in the synthesized tool response.
|
|
49
|
+
|
|
50
|
+
Ignores tools that have corresponding retry prompts as the model is already
|
|
51
|
+
addressing them.
|
|
52
|
+
"""
|
|
53
|
+
if state_manager is None:
|
|
54
|
+
raise ValueError("state_manager is required for patch_tool_messages")
|
|
55
|
+
|
|
56
|
+
messages = state_manager.session.messages
|
|
57
|
+
|
|
58
|
+
if not messages:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Map tool calls to their tool returns
|
|
62
|
+
tool_calls: dict[ToolCallId, ToolName] = {} # tool_call_id -> tool_name
|
|
63
|
+
tool_returns: set[ToolCallId] = set() # set of tool_call_ids with returns
|
|
64
|
+
retry_prompts: set[ToolCallId] = set() # set of tool_call_ids with retry prompts
|
|
65
|
+
|
|
66
|
+
for message in messages:
|
|
67
|
+
if hasattr(message, "parts"):
|
|
68
|
+
for part in message.parts:
|
|
69
|
+
if (
|
|
70
|
+
hasattr(part, "part_kind")
|
|
71
|
+
and hasattr(part, "tool_call_id")
|
|
72
|
+
and part.tool_call_id
|
|
73
|
+
):
|
|
74
|
+
if part.part_kind == "tool-call":
|
|
75
|
+
tool_calls[part.tool_call_id] = part.tool_name
|
|
76
|
+
elif part.part_kind == "tool-return":
|
|
77
|
+
tool_returns.add(part.tool_call_id)
|
|
78
|
+
elif part.part_kind == "retry-prompt":
|
|
79
|
+
retry_prompts.add(part.tool_call_id)
|
|
80
|
+
|
|
81
|
+
# Identify orphaned tools (those without responses and not being retried)
|
|
82
|
+
for tool_call_id, tool_name in list(tool_calls.items()):
|
|
83
|
+
if tool_call_id not in tool_returns and tool_call_id not in retry_prompts:
|
|
84
|
+
# Import ModelRequest and ToolReturnPart lazily
|
|
85
|
+
ModelRequest, ToolReturnPart, _ = get_model_messages()
|
|
86
|
+
messages.append(
|
|
87
|
+
ModelRequest(
|
|
88
|
+
parts=[
|
|
89
|
+
ToolReturnPart(
|
|
90
|
+
tool_name=tool_name,
|
|
91
|
+
content=error_message,
|
|
92
|
+
tool_call_id=tool_call_id,
|
|
93
|
+
timestamp=datetime.now(UTC),
|
|
94
|
+
part_kind="tool-return",
|
|
95
|
+
)
|
|
96
|
+
],
|
|
97
|
+
kind="request",
|
|
98
|
+
)
|
|
99
|
+
)
|