tunacode-cli 0.0.40__py3-none-any.whl → 0.0.42__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/cli/commands/__init__.py +2 -0
- tunacode/cli/commands/implementations/__init__.py +3 -0
- tunacode/cli/commands/implementations/debug.py +1 -1
- tunacode/cli/commands/implementations/todo.py +217 -0
- tunacode/cli/commands/registry.py +2 -0
- tunacode/cli/main.py +12 -5
- tunacode/cli/repl.py +205 -136
- tunacode/configuration/defaults.py +2 -0
- tunacode/configuration/models.py +6 -0
- tunacode/constants.py +27 -3
- tunacode/context.py +7 -3
- tunacode/core/agents/dspy_integration.py +223 -0
- tunacode/core/agents/dspy_tunacode.py +458 -0
- tunacode/core/agents/main.py +182 -12
- tunacode/core/agents/utils.py +54 -6
- tunacode/core/recursive/__init__.py +18 -0
- tunacode/core/recursive/aggregator.py +467 -0
- tunacode/core/recursive/budget.py +414 -0
- tunacode/core/recursive/decomposer.py +398 -0
- tunacode/core/recursive/executor.py +467 -0
- tunacode/core/recursive/hierarchy.py +487 -0
- tunacode/core/setup/config_setup.py +5 -0
- tunacode/core/state.py +91 -1
- tunacode/core/token_usage/api_response_parser.py +44 -0
- tunacode/core/token_usage/cost_calculator.py +58 -0
- tunacode/core/token_usage/usage_tracker.py +98 -0
- tunacode/exceptions.py +23 -0
- tunacode/prompts/dspy_task_planning.md +45 -0
- tunacode/prompts/dspy_tool_selection.md +58 -0
- tunacode/prompts/system.md +69 -5
- tunacode/tools/todo.py +343 -0
- tunacode/types.py +20 -1
- tunacode/ui/console.py +1 -1
- tunacode/ui/input.py +1 -1
- tunacode/ui/output.py +38 -1
- tunacode/ui/panels.py +4 -1
- tunacode/ui/recursive_progress.py +380 -0
- tunacode/ui/tool_ui.py +24 -6
- tunacode/ui/utils.py +1 -1
- tunacode/utils/message_utils.py +17 -0
- tunacode/utils/retry.py +163 -0
- tunacode/utils/token_counter.py +78 -8
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/METADATA +4 -1
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/RECORD +48 -32
- tunacode/cli/textual_app.py +0 -420
- tunacode/cli/textual_bridge.py +0 -161
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.40.dist-info → tunacode_cli-0.0.42.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Module: tunacode.core.recursive.aggregator
|
|
2
|
+
|
|
3
|
+
Result aggregation and context management for recursive task execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from tunacode.core.state import StateManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AggregationStrategy(Enum):
|
|
19
|
+
"""Strategies for aggregating results."""
|
|
20
|
+
|
|
21
|
+
CONCATENATE = "concatenate" # Simple concatenation
|
|
22
|
+
STRUCTURED = "structured" # Preserve task structure
|
|
23
|
+
SUMMARY = "summary" # Generate summary
|
|
24
|
+
INTELLIGENT = "intelligent" # Use agent to merge
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConflictResolution(Enum):
|
|
28
|
+
"""Strategies for resolving conflicts in results."""
|
|
29
|
+
|
|
30
|
+
LATEST = "latest" # Use most recent result
|
|
31
|
+
PRIORITY = "priority" # Use highest priority result
|
|
32
|
+
MERGE = "merge" # Attempt to merge
|
|
33
|
+
AGENT = "agent" # Use agent to resolve
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TaskResult:
|
|
38
|
+
"""Result from a single task execution."""
|
|
39
|
+
|
|
40
|
+
task_id: str
|
|
41
|
+
task_title: str
|
|
42
|
+
result_data: Any
|
|
43
|
+
status: str # completed, failed, partial
|
|
44
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
45
|
+
error: Optional[str] = None
|
|
46
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AggregatedResult:
|
|
51
|
+
"""Aggregated result from multiple tasks."""
|
|
52
|
+
|
|
53
|
+
primary_result: Any
|
|
54
|
+
task_results: List[TaskResult]
|
|
55
|
+
strategy_used: AggregationStrategy
|
|
56
|
+
conflicts_resolved: int = 0
|
|
57
|
+
partial_failures: List[str] = field(default_factory=list)
|
|
58
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
aggregation_time: datetime = field(default_factory=datetime.now)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ExecutionContext:
|
|
64
|
+
"""Context information during execution."""
|
|
65
|
+
|
|
66
|
+
task_id: str
|
|
67
|
+
parent_context: Dict[str, Any]
|
|
68
|
+
local_context: Dict[str, Any]
|
|
69
|
+
files_accessed: Set[str] = field(default_factory=set)
|
|
70
|
+
tools_used: List[str] = field(default_factory=list)
|
|
71
|
+
key_findings: List[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ResultAggregator:
|
|
75
|
+
"""Aggregates results from distributed subtask executions."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, state_manager: StateManager):
|
|
78
|
+
"""Initialize the ResultAggregator.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
state_manager: StateManager for accessing agents
|
|
82
|
+
"""
|
|
83
|
+
self.state_manager = state_manager
|
|
84
|
+
self._context_cache: Dict[str, ExecutionContext] = {}
|
|
85
|
+
|
|
86
|
+
async def aggregate_results(
|
|
87
|
+
self,
|
|
88
|
+
task_results: List[TaskResult],
|
|
89
|
+
parent_task: Optional[Dict[str, Any]] = None,
|
|
90
|
+
strategy: AggregationStrategy = AggregationStrategy.INTELLIGENT,
|
|
91
|
+
) -> AggregatedResult:
|
|
92
|
+
"""Aggregate results from multiple subtasks.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
task_results: List of results from subtasks
|
|
96
|
+
parent_task: Optional parent task information
|
|
97
|
+
strategy: Aggregation strategy to use
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
AggregatedResult with merged data
|
|
101
|
+
"""
|
|
102
|
+
if not task_results:
|
|
103
|
+
return AggregatedResult(
|
|
104
|
+
primary_result="No results to aggregate", task_results=[], strategy_used=strategy
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Separate successful and failed results
|
|
108
|
+
successful_results = [r for r in task_results if r.status == "completed"]
|
|
109
|
+
failed_results = [r for r in task_results if r.status == "failed"]
|
|
110
|
+
partial_results = [r for r in task_results if r.status == "partial"]
|
|
111
|
+
|
|
112
|
+
# Handle complete failure
|
|
113
|
+
if not successful_results and not partial_results:
|
|
114
|
+
return AggregatedResult(
|
|
115
|
+
primary_result="All subtasks failed",
|
|
116
|
+
task_results=task_results,
|
|
117
|
+
strategy_used=strategy,
|
|
118
|
+
partial_failures=[r.task_id for r in failed_results],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Apply aggregation strategy
|
|
122
|
+
if strategy == AggregationStrategy.CONCATENATE:
|
|
123
|
+
result = await self._aggregate_concatenate(successful_results + partial_results)
|
|
124
|
+
elif strategy == AggregationStrategy.STRUCTURED:
|
|
125
|
+
result = await self._aggregate_structured(
|
|
126
|
+
successful_results + partial_results, parent_task
|
|
127
|
+
)
|
|
128
|
+
elif strategy == AggregationStrategy.SUMMARY:
|
|
129
|
+
result = await self._aggregate_summary(
|
|
130
|
+
successful_results + partial_results, parent_task
|
|
131
|
+
)
|
|
132
|
+
elif strategy == AggregationStrategy.INTELLIGENT:
|
|
133
|
+
result = await self._aggregate_intelligent(
|
|
134
|
+
successful_results + partial_results, parent_task
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
result = await self._aggregate_concatenate(successful_results + partial_results)
|
|
138
|
+
|
|
139
|
+
return AggregatedResult(
|
|
140
|
+
primary_result=result,
|
|
141
|
+
task_results=task_results,
|
|
142
|
+
strategy_used=strategy,
|
|
143
|
+
partial_failures=[r.task_id for r in failed_results],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async def _aggregate_concatenate(self, results: List[TaskResult]) -> str:
|
|
147
|
+
"""Simple concatenation of results.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
results: List of successful results
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Concatenated string result
|
|
154
|
+
"""
|
|
155
|
+
parts = []
|
|
156
|
+
for i, result in enumerate(results):
|
|
157
|
+
parts.append(f"=== Task {i + 1}: {result.task_title} ===")
|
|
158
|
+
parts.append(str(result.result_data))
|
|
159
|
+
parts.append("")
|
|
160
|
+
|
|
161
|
+
return "\n".join(parts)
|
|
162
|
+
|
|
163
|
+
async def _aggregate_structured(
|
|
164
|
+
self, results: List[TaskResult], parent_task: Optional[Dict[str, Any]]
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
"""Create structured aggregation preserving task relationships.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
results: List of successful results
|
|
170
|
+
parent_task: Parent task information
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Structured dictionary result
|
|
174
|
+
"""
|
|
175
|
+
structured_result = {
|
|
176
|
+
"parent_task": parent_task.get("title") if parent_task else "Unknown",
|
|
177
|
+
"completed_subtasks": len([r for r in results if r.status == "completed"]),
|
|
178
|
+
"partial_subtasks": len([r for r in results if r.status == "partial"]),
|
|
179
|
+
"subtask_results": [],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for result in results:
|
|
183
|
+
structured_result["subtask_results"].append(
|
|
184
|
+
{
|
|
185
|
+
"task_id": result.task_id,
|
|
186
|
+
"title": result.task_title,
|
|
187
|
+
"status": result.status,
|
|
188
|
+
"result": result.result_data,
|
|
189
|
+
"timestamp": result.timestamp.isoformat(),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return structured_result
|
|
194
|
+
|
|
195
|
+
async def _aggregate_summary(
|
|
196
|
+
self, results: List[TaskResult], parent_task: Optional[Dict[str, Any]]
|
|
197
|
+
) -> str:
|
|
198
|
+
"""Generate a summary of results.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
results: List of successful results
|
|
202
|
+
parent_task: Parent task information
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Summary string
|
|
206
|
+
"""
|
|
207
|
+
summary_parts = []
|
|
208
|
+
|
|
209
|
+
if parent_task:
|
|
210
|
+
summary_parts.append(f"Summary for: {parent_task.get('title', 'Unknown Task')}")
|
|
211
|
+
summary_parts.append("")
|
|
212
|
+
|
|
213
|
+
summary_parts.append(f"Completed {len(results)} subtasks:")
|
|
214
|
+
summary_parts.append("")
|
|
215
|
+
|
|
216
|
+
for i, result in enumerate(results):
|
|
217
|
+
# Extract key information from result
|
|
218
|
+
result_str = str(result.result_data)
|
|
219
|
+
preview = result_str[:200] + "..." if len(result_str) > 200 else result_str
|
|
220
|
+
|
|
221
|
+
summary_parts.append(f"{i + 1}. {result.task_title}")
|
|
222
|
+
summary_parts.append(f" Status: {result.status}")
|
|
223
|
+
summary_parts.append(f" Result: {preview}")
|
|
224
|
+
summary_parts.append("")
|
|
225
|
+
|
|
226
|
+
return "\n".join(summary_parts)
|
|
227
|
+
|
|
228
|
+
async def _aggregate_intelligent(
|
|
229
|
+
self, results: List[TaskResult], parent_task: Optional[Dict[str, Any]]
|
|
230
|
+
) -> Any:
|
|
231
|
+
"""Use agent to intelligently merge results.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
results: List of successful results
|
|
235
|
+
parent_task: Parent task information
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Intelligently merged result
|
|
239
|
+
"""
|
|
240
|
+
agent = self.state_manager.session.agents.get("main")
|
|
241
|
+
if not agent:
|
|
242
|
+
# Fallback to summary
|
|
243
|
+
return await self._aggregate_summary(results, parent_task)
|
|
244
|
+
|
|
245
|
+
# Prepare context for agent
|
|
246
|
+
context = {
|
|
247
|
+
"parent_task": parent_task.get("description") if parent_task else "Unknown",
|
|
248
|
+
"subtask_count": len(results),
|
|
249
|
+
"subtask_results": [],
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for result in results:
|
|
253
|
+
context["subtask_results"].append(
|
|
254
|
+
{
|
|
255
|
+
"title": result.task_title,
|
|
256
|
+
"result": str(result.result_data)[:500], # Limit size
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
merge_prompt = f"""Intelligently merge and synthesize these subtask results into a coherent response.
|
|
261
|
+
|
|
262
|
+
Parent Task: {context["parent_task"]}
|
|
263
|
+
|
|
264
|
+
Subtask Results:
|
|
265
|
+
{json.dumps(context["subtask_results"], indent=2)}
|
|
266
|
+
|
|
267
|
+
Instructions:
|
|
268
|
+
1. Identify the key achievements from each subtask
|
|
269
|
+
2. Synthesize findings into a unified response
|
|
270
|
+
3. Highlight any important patterns or insights
|
|
271
|
+
4. Present the result in a clear, actionable format
|
|
272
|
+
|
|
273
|
+
Provide a comprehensive but concise synthesis that addresses the original task."""
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
result = await agent.run(merge_prompt)
|
|
277
|
+
return result
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Agent-based aggregation failed: {str(e)}")
|
|
280
|
+
# Fallback to summary
|
|
281
|
+
return await self._aggregate_summary(results, parent_task)
|
|
282
|
+
|
|
283
|
+
def create_context(
|
|
284
|
+
self, task_id: str, parent_context: Optional[Dict[str, Any]] = None
|
|
285
|
+
) -> ExecutionContext:
|
|
286
|
+
"""Create an execution context for a task.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
task_id: Task identifier
|
|
290
|
+
parent_context: Optional parent context to inherit
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
New execution context
|
|
294
|
+
"""
|
|
295
|
+
context = ExecutionContext(
|
|
296
|
+
task_id=task_id, parent_context=parent_context or {}, local_context={}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
self._context_cache[task_id] = context
|
|
300
|
+
return context
|
|
301
|
+
|
|
302
|
+
def get_context(self, task_id: str) -> Optional[ExecutionContext]:
|
|
303
|
+
"""Get execution context for a task.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
task_id: Task identifier
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Execution context or None
|
|
310
|
+
"""
|
|
311
|
+
return self._context_cache.get(task_id)
|
|
312
|
+
|
|
313
|
+
def update_context(
|
|
314
|
+
self,
|
|
315
|
+
task_id: str,
|
|
316
|
+
updates: Dict[str, Any],
|
|
317
|
+
files: Optional[Set[str]] = None,
|
|
318
|
+
tools: Optional[List[str]] = None,
|
|
319
|
+
findings: Optional[List[str]] = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Update execution context for a task.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
task_id: Task identifier
|
|
325
|
+
updates: Context updates
|
|
326
|
+
files: Files accessed
|
|
327
|
+
tools: Tools used
|
|
328
|
+
findings: Key findings
|
|
329
|
+
"""
|
|
330
|
+
context = self._context_cache.get(task_id)
|
|
331
|
+
if not context:
|
|
332
|
+
logger.warning(f"No context found for task {task_id}")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
context.local_context.update(updates)
|
|
336
|
+
|
|
337
|
+
if files:
|
|
338
|
+
context.files_accessed.update(files)
|
|
339
|
+
if tools:
|
|
340
|
+
context.tools_used.extend(tools)
|
|
341
|
+
if findings:
|
|
342
|
+
context.key_findings.extend(findings)
|
|
343
|
+
|
|
344
|
+
def synthesize_contexts(self, task_ids: List[str]) -> Dict[str, Any]:
|
|
345
|
+
"""Synthesize contexts from multiple tasks.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
task_ids: List of task IDs
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Synthesized context dictionary
|
|
352
|
+
"""
|
|
353
|
+
synthesized = {
|
|
354
|
+
"files_accessed": set(),
|
|
355
|
+
"tools_used": [],
|
|
356
|
+
"key_findings": [],
|
|
357
|
+
"combined_context": {},
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for task_id in task_ids:
|
|
361
|
+
context = self._context_cache.get(task_id)
|
|
362
|
+
if context:
|
|
363
|
+
synthesized["files_accessed"].update(context.files_accessed)
|
|
364
|
+
synthesized["tools_used"].extend(context.tools_used)
|
|
365
|
+
synthesized["key_findings"].extend(context.key_findings)
|
|
366
|
+
synthesized["combined_context"].update(context.local_context)
|
|
367
|
+
|
|
368
|
+
# Convert set to list for JSON serialization
|
|
369
|
+
synthesized["files_accessed"] = list(synthesized["files_accessed"])
|
|
370
|
+
|
|
371
|
+
# Remove duplicates from tools while preserving order
|
|
372
|
+
seen = set()
|
|
373
|
+
unique_tools = []
|
|
374
|
+
for tool in synthesized["tools_used"]:
|
|
375
|
+
if tool not in seen:
|
|
376
|
+
seen.add(tool)
|
|
377
|
+
unique_tools.append(tool)
|
|
378
|
+
synthesized["tools_used"] = unique_tools
|
|
379
|
+
|
|
380
|
+
return synthesized
|
|
381
|
+
|
|
382
|
+
async def resolve_conflicts(
|
|
383
|
+
self,
|
|
384
|
+
conflicting_results: List[TaskResult],
|
|
385
|
+
strategy: ConflictResolution = ConflictResolution.AGENT,
|
|
386
|
+
) -> TaskResult:
|
|
387
|
+
"""Resolve conflicts between contradictory results.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
conflicting_results: List of conflicting results
|
|
391
|
+
strategy: Resolution strategy
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Single resolved result
|
|
395
|
+
"""
|
|
396
|
+
if not conflicting_results:
|
|
397
|
+
raise ValueError("No results to resolve conflicts for")
|
|
398
|
+
|
|
399
|
+
if len(conflicting_results) == 1:
|
|
400
|
+
return conflicting_results[0]
|
|
401
|
+
|
|
402
|
+
if strategy == ConflictResolution.LATEST:
|
|
403
|
+
# Return most recent result
|
|
404
|
+
return max(conflicting_results, key=lambda r: r.timestamp)
|
|
405
|
+
|
|
406
|
+
elif strategy == ConflictResolution.PRIORITY:
|
|
407
|
+
# Use metadata priority if available
|
|
408
|
+
def get_priority(result: TaskResult) -> int:
|
|
409
|
+
return result.metadata.get("priority", 0)
|
|
410
|
+
|
|
411
|
+
return max(conflicting_results, key=get_priority)
|
|
412
|
+
|
|
413
|
+
elif strategy == ConflictResolution.MERGE:
|
|
414
|
+
# Simple merge attempt
|
|
415
|
+
merged_data = {}
|
|
416
|
+
for result in conflicting_results:
|
|
417
|
+
if isinstance(result.result_data, dict):
|
|
418
|
+
merged_data.update(result.result_data)
|
|
419
|
+
else:
|
|
420
|
+
# Can't merge non-dict results
|
|
421
|
+
return conflicting_results[-1]
|
|
422
|
+
|
|
423
|
+
return TaskResult(
|
|
424
|
+
task_id="merged",
|
|
425
|
+
task_title="Merged Result",
|
|
426
|
+
result_data=merged_data,
|
|
427
|
+
status="completed",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
elif strategy == ConflictResolution.AGENT:
|
|
431
|
+
# Use agent to resolve
|
|
432
|
+
agent = self.state_manager.session.agents.get("main")
|
|
433
|
+
if not agent:
|
|
434
|
+
# Fallback to latest
|
|
435
|
+
return max(conflicting_results, key=lambda r: r.timestamp)
|
|
436
|
+
|
|
437
|
+
conflict_data = []
|
|
438
|
+
for result in conflicting_results:
|
|
439
|
+
conflict_data.append(
|
|
440
|
+
{
|
|
441
|
+
"task": result.task_title,
|
|
442
|
+
"result": str(result.result_data)[:300],
|
|
443
|
+
"timestamp": result.timestamp.isoformat(),
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
resolution_prompt = f"""Resolve conflicts between these task results:
|
|
448
|
+
|
|
449
|
+
{json.dumps(conflict_data, indent=2)}
|
|
450
|
+
|
|
451
|
+
Analyze the differences and provide a single, coherent result that best represents the correct outcome."""
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
resolved = await agent.run(resolution_prompt)
|
|
455
|
+
return TaskResult(
|
|
456
|
+
task_id="resolved",
|
|
457
|
+
task_title="Conflict Resolved",
|
|
458
|
+
result_data=resolved,
|
|
459
|
+
status="completed",
|
|
460
|
+
)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Agent conflict resolution failed: {str(e)}")
|
|
463
|
+
# Fallback to latest
|
|
464
|
+
return max(conflicting_results, key=lambda r: r.timestamp)
|
|
465
|
+
|
|
466
|
+
# Default to latest
|
|
467
|
+
return max(conflicting_results, key=lambda r: r.timestamp)
|