tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__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.

Files changed (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,340 @@
1
+ """
2
+ Module: tunacode.utils.config_comparator
3
+
4
+ Configuration comparison utility for analyzing user configurations against defaults.
5
+ Provides detailed analysis of customizations, defaults, and configuration state.
6
+ """
7
+
8
+ import json
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Set, Union
12
+
13
+ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
14
+ from tunacode.types import UserConfig
15
+
16
+
17
+ @dataclass
18
+ class ConfigDifference:
19
+ """Represents a difference between user config and defaults."""
20
+
21
+ key_path: str
22
+ user_value: Any
23
+ default_value: Any
24
+ difference_type: str # "custom", "missing", "extra", "type_mismatch"
25
+ section: str
26
+ description: str
27
+
28
+
29
+ @dataclass
30
+ class ConfigAnalysis:
31
+ """Complete analysis of configuration state."""
32
+
33
+ user_config: UserConfig
34
+ default_config: UserConfig
35
+ differences: List[ConfigDifference]
36
+ custom_keys: Set[str]
37
+ missing_keys: Set[str]
38
+ extra_keys: Set[str]
39
+ type_mismatches: Set[str]
40
+ sections_analyzed: Set[str]
41
+ total_keys: int
42
+ custom_percentage: float
43
+
44
+
45
+ class ConfigComparator:
46
+ """Compares user configuration against defaults to identify customizations."""
47
+
48
+ def __init__(self, default_config: Optional[UserConfig] = None):
49
+ """Initialize comparator with default configuration."""
50
+ self.default_config = default_config or DEFAULT_USER_CONFIG
51
+
52
+ def _get_key_description(self, key_path: str, difference_type: str) -> str:
53
+ """Get a descriptive explanation for a configuration key difference."""
54
+ try:
55
+ from tunacode.configuration.key_descriptions import get_key_description
56
+
57
+ desc = get_key_description(key_path)
58
+
59
+ if desc:
60
+ if difference_type == "custom":
61
+ return f"Custom: {desc.description}"
62
+ elif difference_type == "missing":
63
+ return f"Missing: {desc.description}"
64
+ elif difference_type == "extra":
65
+ return f"Extra: {desc.description}"
66
+ elif difference_type == "type_mismatch":
67
+ return f"Type mismatch: {desc.description}"
68
+
69
+ except ImportError:
70
+ pass # Fall back to basic descriptions
71
+
72
+ # Fallback descriptions
73
+ if difference_type == "custom":
74
+ return f"Custom value: {key_path}"
75
+ elif difference_type == "missing":
76
+ return f"Missing configuration key: {key_path}"
77
+ elif difference_type == "extra":
78
+ return f"Extra configuration key: {key_path}"
79
+ elif difference_type == "type_mismatch":
80
+ return f"Type mismatch for: {key_path}"
81
+
82
+ return f"Configuration difference: {key_path}"
83
+
84
+ def analyze_config(self, user_config: UserConfig) -> ConfigAnalysis:
85
+ """Perform complete analysis of user configuration."""
86
+ differences: list[ConfigDifference] = []
87
+ custom_keys: set[str] = set()
88
+ missing_keys: set[str] = set()
89
+ extra_keys: set[str] = set()
90
+ type_mismatches: set[str] = set()
91
+
92
+ # Analyze each section recursively
93
+ self._analyze_recursive(
94
+ user_config=user_config,
95
+ default_config=self.default_config,
96
+ current_path="",
97
+ differences=differences,
98
+ custom_keys=custom_keys,
99
+ missing_keys=missing_keys,
100
+ extra_keys=extra_keys,
101
+ type_mismatches=type_mismatches,
102
+ )
103
+
104
+ # Calculate statistics
105
+ sections_analyzed: set[str] = set()
106
+ self._collect_sections(user_config, sections_analyzed)
107
+ self._collect_sections(self.default_config, sections_analyzed)
108
+
109
+ total_keys = len(custom_keys) + len(missing_keys) + len(extra_keys) + len(type_mismatches)
110
+ custom_percentage = (len(custom_keys) / total_keys * 100) if total_keys > 0 else 0
111
+
112
+ return ConfigAnalysis(
113
+ user_config=user_config,
114
+ default_config=self.default_config,
115
+ differences=differences,
116
+ custom_keys=custom_keys,
117
+ missing_keys=missing_keys,
118
+ extra_keys=extra_keys,
119
+ type_mismatches=type_mismatches,
120
+ sections_analyzed=sections_analyzed,
121
+ total_keys=total_keys,
122
+ custom_percentage=custom_percentage,
123
+ )
124
+
125
+ def _analyze_recursive(
126
+ self,
127
+ user_config: Dict[str, Any],
128
+ default_config: Dict[str, Any],
129
+ current_path: str,
130
+ differences: List[ConfigDifference],
131
+ custom_keys: Set[str],
132
+ missing_keys: Set[str],
133
+ extra_keys: Set[str],
134
+ type_mismatches: Set[str],
135
+ ) -> None:
136
+ """Recursively analyze configuration differences."""
137
+
138
+ # Check for missing keys (present in default but not in user)
139
+ for key, default_value in default_config.items():
140
+ full_key = f"{current_path}.{key}" if current_path else key
141
+
142
+ if key not in user_config:
143
+ missing_keys.add(full_key)
144
+ differences.append(
145
+ ConfigDifference(
146
+ key_path=full_key,
147
+ user_value=None,
148
+ default_value=default_value,
149
+ difference_type="missing",
150
+ section=current_path or "root",
151
+ description=self._get_key_description(full_key, "missing"),
152
+ )
153
+ )
154
+ continue
155
+
156
+ user_value = user_config[key]
157
+
158
+ # Recursively analyze nested dictionaries
159
+ if isinstance(default_value, dict) and isinstance(user_value, dict):
160
+ self._analyze_recursive(
161
+ user_config=user_value,
162
+ default_config=default_value,
163
+ current_path=full_key,
164
+ differences=differences,
165
+ custom_keys=custom_keys,
166
+ missing_keys=missing_keys,
167
+ extra_keys=extra_keys,
168
+ type_mismatches=type_mismatches,
169
+ )
170
+ continue
171
+
172
+ # Check for type mismatches
173
+ if not isinstance(user_value, type(default_value)):
174
+ type_mismatches.add(full_key)
175
+ differences.append(
176
+ ConfigDifference(
177
+ key_path=full_key,
178
+ user_value=user_value,
179
+ default_value=default_value,
180
+ difference_type="type_mismatch",
181
+ section=current_path or "root",
182
+ description=self._get_key_description(full_key, "type_mismatch"),
183
+ )
184
+ )
185
+ continue
186
+
187
+ # Check for custom values
188
+ if user_value != default_value:
189
+ custom_keys.add(full_key)
190
+ differences.append(
191
+ ConfigDifference(
192
+ key_path=full_key,
193
+ user_value=user_value,
194
+ default_value=default_value,
195
+ difference_type="custom",
196
+ section=current_path or "root",
197
+ description=self._get_key_description(full_key, "custom"),
198
+ )
199
+ )
200
+
201
+ # Check for extra keys (present in user but not in default)
202
+ for key, user_value in user_config.items():
203
+ if key not in default_config:
204
+ full_key = f"{current_path}.{key}" if current_path else key
205
+ extra_keys.add(full_key)
206
+ differences.append(
207
+ ConfigDifference(
208
+ key_path=full_key,
209
+ user_value=user_value,
210
+ default_value=None,
211
+ difference_type="extra",
212
+ section=current_path or "root",
213
+ description=self._get_key_description(full_key, "extra"),
214
+ )
215
+ )
216
+
217
+ def _collect_sections(self, config: Dict[str, Any], sections: Set[str]) -> None:
218
+ """Collect all section names from configuration."""
219
+ for key, value in config.items():
220
+ if isinstance(value, dict):
221
+ sections.add(key)
222
+ self._collect_sections(value, sections)
223
+
224
+ def get_summary_stats(self, analysis: ConfigAnalysis) -> Dict[str, Any]:
225
+ """Get summary statistics for the configuration analysis."""
226
+ return {
227
+ "total_keys_analyzed": analysis.total_keys,
228
+ "custom_keys_count": len(analysis.custom_keys),
229
+ "missing_keys_count": len(analysis.missing_keys),
230
+ "extra_keys_count": len(analysis.extra_keys),
231
+ "type_mismatches_count": len(analysis.type_mismatches),
232
+ "custom_percentage": analysis.custom_percentage,
233
+ "sections_analyzed": len(analysis.sections_analyzed),
234
+ "has_issues": bool(analysis.missing_keys or analysis.type_mismatches),
235
+ }
236
+
237
+ def get_section_analysis(
238
+ self, analysis: ConfigAnalysis, section: str
239
+ ) -> List[ConfigDifference]:
240
+ """Get differences for a specific section."""
241
+ return [diff for diff in analysis.differences if diff.section == section]
242
+
243
+ def is_config_healthy(self, analysis: ConfigAnalysis) -> bool:
244
+ """Check if configuration is healthy (no critical issues)."""
245
+ # Type mismatches are considered critical
246
+ if analysis.type_mismatches:
247
+ return False
248
+
249
+ # Missing keys might be acceptable depending on the context
250
+ # For now, we'll consider missing keys as warnings, not errors
251
+ return True
252
+
253
+ def get_recommendations(self, analysis: ConfigAnalysis) -> List[str]:
254
+ """Get recommendations based on configuration analysis."""
255
+ recommendations = []
256
+
257
+ if analysis.type_mismatches:
258
+ recommendations.append(
259
+ f"Fix {len(analysis.type_mismatches)} type mismatch(es) in configuration"
260
+ )
261
+
262
+ if analysis.missing_keys:
263
+ recommendations.append(
264
+ f"Consider adding {len(analysis.missing_keys)} missing configuration key(s)"
265
+ )
266
+
267
+ if analysis.custom_percentage > 80:
268
+ recommendations.append(
269
+ "High customization detected - consider documenting your configuration"
270
+ )
271
+
272
+ if analysis.extra_keys:
273
+ recommendations.append(
274
+ f"Found {len(analysis.extra_keys)} unrecognized configuration key(s)"
275
+ )
276
+
277
+ return recommendations
278
+
279
+
280
+ def load_and_analyze_config(config_path: Optional[Union[str, Path]] = None) -> ConfigAnalysis:
281
+ """Load configuration from file and analyze it."""
282
+ from tunacode.utils.user_configuration import load_config
283
+
284
+ if config_path:
285
+ try:
286
+ with open(config_path, "r") as f:
287
+ user_config = json.load(f)
288
+ except (FileNotFoundError, json.JSONDecodeError) as e:
289
+ raise ValueError(f"Failed to load config from {config_path}: {e}")
290
+ else:
291
+ user_config = load_config()
292
+ if user_config is None:
293
+ raise ValueError("No user configuration found")
294
+
295
+ comparator = ConfigComparator()
296
+ return comparator.analyze_config(user_config)
297
+
298
+
299
+ def create_config_report(analysis: ConfigAnalysis) -> str:
300
+ """Create a human-readable report of configuration analysis."""
301
+ stats = ConfigComparator().get_summary_stats(analysis)
302
+
303
+ report = [
304
+ "Configuration Analysis Report",
305
+ "=" * 50,
306
+ f"Total keys analyzed: {stats['total_keys_analyzed']}",
307
+ f"Custom keys: {stats['custom_keys_count']} ({stats['custom_percentage']:.1f}%)",
308
+ f"Missing keys: {stats['missing_keys_count']}",
309
+ f"Extra keys: {stats['extra_keys_count']}",
310
+ f"Type mismatches: {stats['type_mismatches_count']}",
311
+ f"Sections analyzed: {stats['sections_analyzed']}",
312
+ f"Configuration healthy: {'Yes' if stats['has_issues'] else 'No'}",
313
+ "",
314
+ ]
315
+
316
+ if analysis.custom_keys:
317
+ report.append("Custom Values:")
318
+ for key in sorted(analysis.custom_keys):
319
+ report.append(f" ✓ {key}")
320
+ report.append("")
321
+
322
+ if analysis.missing_keys:
323
+ report.append("Missing Keys:")
324
+ for key in sorted(analysis.missing_keys):
325
+ report.append(f" ⚠ {key}")
326
+ report.append("")
327
+
328
+ if analysis.type_mismatches:
329
+ report.append("Type Mismatches:")
330
+ for key in sorted(analysis.type_mismatches):
331
+ report.append(f" ✗ {key}")
332
+ report.append("")
333
+
334
+ recommendations = ConfigComparator().get_recommendations(analysis)
335
+ if recommendations:
336
+ report.append("Recommendations:")
337
+ for rec in recommendations:
338
+ report.append(f" • {rec}")
339
+
340
+ return "\n".join(report)
@@ -0,0 +1,206 @@
1
+ """
2
+ Module: tunacode.utils.json_utils
3
+
4
+ JSON parsing utilities with enhanced error handling and concatenated object support.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from tunacode.constants import READ_ONLY_TOOLS
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ConcatenatedJSONError(Exception):
17
+ """Raised when concatenated JSON objects are detected but cannot be safely handled."""
18
+
19
+ def __init__(self, message: str, objects_found: int, tool_name: Optional[str] = None):
20
+ self.message = message
21
+ self.objects_found = objects_found
22
+ self.tool_name = tool_name
23
+ super().__init__(message)
24
+
25
+
26
+ def split_concatenated_json(json_string: str, strict_mode: bool = True) -> List[Dict[str, Any]]:
27
+ """
28
+ Split concatenated JSON objects like {"a": 1}{"b": 2} into separate objects.
29
+
30
+ Args:
31
+ json_string: String containing potentially concatenated JSON objects
32
+ strict_mode: If True, only returns valid JSON objects. If False, attempts
33
+ to recover partial objects.
34
+
35
+ Returns:
36
+ List of parsed JSON objects
37
+
38
+ Raises:
39
+ json.JSONDecodeError: If no valid JSON objects can be extracted
40
+ ConcatenatedJSONError: If multiple objects found but not safe to process
41
+ """
42
+ objects = []
43
+ brace_count = 0
44
+ start_pos = 0
45
+ in_string = False
46
+ escape_next = False
47
+
48
+ for i, char in enumerate(json_string):
49
+ if escape_next:
50
+ escape_next = False
51
+ continue
52
+
53
+ if char == "\\":
54
+ escape_next = True
55
+ continue
56
+
57
+ if char == '"' and not escape_next:
58
+ in_string = not in_string
59
+ continue
60
+
61
+ if in_string:
62
+ continue
63
+
64
+ if char == "{":
65
+ if brace_count == 0:
66
+ start_pos = i
67
+ brace_count += 1
68
+ elif char == "}":
69
+ brace_count -= 1
70
+ if brace_count == 0:
71
+ potential_json = json_string[start_pos : i + 1].strip()
72
+ try:
73
+ parsed = json.loads(potential_json)
74
+ if isinstance(parsed, dict):
75
+ objects.append(parsed)
76
+ else:
77
+ logger.warning(f"Non-dict JSON object ignored: {type(parsed)}")
78
+ except json.JSONDecodeError as e:
79
+ if strict_mode:
80
+ logger.debug(f"Invalid JSON fragment skipped: {potential_json[:100]}...")
81
+ else:
82
+ logger.warning(f"JSON parse error: {e}")
83
+ continue
84
+
85
+ if not objects:
86
+ raise json.JSONDecodeError("No valid JSON objects found", json_string, 0)
87
+
88
+ return objects
89
+
90
+
91
+ def validate_tool_args_safety(
92
+ objects: List[Dict[str, Any]], tool_name: Optional[str] = None
93
+ ) -> bool:
94
+ """
95
+ Validate whether it's safe to execute multiple JSON objects for a given tool.
96
+
97
+ Args:
98
+ objects: List of JSON objects to validate
99
+ tool_name: Name of the tool (if known)
100
+
101
+ Returns:
102
+ bool: True if safe to execute, False otherwise
103
+
104
+ Raises:
105
+ ConcatenatedJSONError: If multiple objects detected for unsafe tool
106
+ """
107
+ if len(objects) <= 1:
108
+ return True
109
+
110
+ # Check if tool is read-only (safer to execute multiple times)
111
+ if tool_name and tool_name in READ_ONLY_TOOLS:
112
+ logger.info(f"Multiple JSON objects for read-only tool {tool_name} - allowing execution")
113
+ return True
114
+
115
+ # For write/execute tools, multiple objects are potentially dangerous
116
+ if tool_name:
117
+ logger.warning(
118
+ f"Multiple JSON objects detected for tool {tool_name} "
119
+ f"({len(objects)} objects). This may indicate a model error."
120
+ )
121
+ raise ConcatenatedJSONError(
122
+ f"Multiple JSON objects not safe for tool {tool_name}",
123
+ objects_found=len(objects),
124
+ tool_name=tool_name,
125
+ )
126
+ else:
127
+ logger.warning(f"Multiple JSON objects detected ({len(objects)}) with unknown tool")
128
+ return False
129
+
130
+
131
+ def safe_json_parse(
132
+ json_string: str, tool_name: Optional[str] = None, allow_concatenated: bool = False
133
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
134
+ """
135
+ Safely parse JSON with optional concatenated object support.
136
+
137
+ Args:
138
+ json_string: JSON string to parse
139
+ tool_name: Name of the tool (for safety validation)
140
+ allow_concatenated: Whether to attempt splitting concatenated objects
141
+
142
+ Returns:
143
+ Single dict if one object, or list of dicts if multiple objects
144
+
145
+ Raises:
146
+ json.JSONDecodeError: If parsing fails
147
+ ConcatenatedJSONError: If concatenated objects are unsafe
148
+ """
149
+ try:
150
+ # First, try normal JSON parsing
151
+ result = json.loads(json_string)
152
+ if isinstance(result, dict):
153
+ return result
154
+ else:
155
+ raise json.JSONDecodeError(f"Expected dict, got {type(result)}", json_string, 0)
156
+
157
+ except json.JSONDecodeError as e:
158
+ if not allow_concatenated or "Extra data" not in str(e):
159
+ raise
160
+
161
+ logger.info("Attempting to split concatenated JSON objects")
162
+
163
+ # Try to split concatenated objects
164
+ objects = split_concatenated_json(json_string)
165
+
166
+ # Validate safety
167
+ if validate_tool_args_safety(objects, tool_name):
168
+ if len(objects) == 1:
169
+ return objects[0]
170
+ else:
171
+ return objects
172
+ else:
173
+ # Not safe - return first object with warning
174
+ logger.warning(f"Using first of {len(objects)} JSON objects only")
175
+ return objects[0]
176
+
177
+
178
+ def merge_json_objects(objects: List[Dict[str, Any]], strategy: str = "first") -> Dict[str, Any]:
179
+ """
180
+ Merge multiple JSON objects using different strategies.
181
+
182
+ Args:
183
+ objects: List of JSON objects to merge
184
+ strategy: Merge strategy ("first", "last", "combine")
185
+
186
+ Returns:
187
+ Single merged JSON object
188
+ """
189
+ if not objects:
190
+ return {}
191
+
192
+ if len(objects) == 1:
193
+ return objects[0]
194
+
195
+ if strategy == "first":
196
+ return objects[0]
197
+ elif strategy == "last":
198
+ return objects[-1]
199
+ elif strategy == "combine":
200
+ # Combine all objects, later values override earlier ones
201
+ result = {}
202
+ for obj in objects:
203
+ result.update(obj)
204
+ return result
205
+ else:
206
+ raise ValueError(f"Unknown merge strategy: {strategy}")
@@ -9,11 +9,21 @@ def get_message_content(message: Any) -> str:
9
9
  return message
10
10
  if isinstance(message, dict):
11
11
  if "content" in message:
12
- return message["content"]
12
+ content = message["content"]
13
+ # Handle nested content structures
14
+ if isinstance(content, list):
15
+ return " ".join(get_message_content(item) for item in content)
16
+ return str(content)
13
17
  if "thought" in message:
14
- return message["thought"]
18
+ return str(message["thought"])
15
19
  if hasattr(message, "content"):
16
- return message.content
20
+ content = message.content
21
+ if isinstance(content, list):
22
+ return " ".join(get_message_content(item) for item in content)
23
+ return str(content)
17
24
  if hasattr(message, "parts"):
18
- return " ".join(get_message_content(part) for part in message.parts)
25
+ parts = message.parts
26
+ if isinstance(parts, list):
27
+ return " ".join(get_message_content(part) for part in parts)
28
+ return str(parts)
19
29
  return ""