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

@@ -1,414 +0,0 @@
1
- """Module: tunacode.core.recursive.budget
2
-
3
- Budget management system for allocating and tracking computational resources across recursive executions.
4
- """
5
-
6
- import logging
7
- from dataclasses import dataclass, field
8
- from datetime import datetime
9
- from enum import Enum
10
- from typing import Dict, List, Optional
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class AllocationStrategy(Enum):
16
- """Budget allocation strategies."""
17
-
18
- EQUAL = "equal" # Equal distribution
19
- WEIGHTED = "weighted" # Based on complexity scores
20
- ADAPTIVE = "adaptive" # Adjusts based on consumption
21
- PRIORITY = "priority" # Based on task priority
22
-
23
-
24
- @dataclass
25
- class BudgetAllocation:
26
- """Represents a budget allocation for a task."""
27
-
28
- task_id: str
29
- allocated_budget: int
30
- consumed_budget: int = 0
31
- remaining_budget: int = field(init=False)
32
- allocation_time: datetime = field(default_factory=datetime.now)
33
- last_update: datetime = field(default_factory=datetime.now)
34
-
35
- def __post_init__(self):
36
- self.remaining_budget = self.allocated_budget - self.consumed_budget
37
-
38
- def consume(self, amount: int) -> bool:
39
- """Consume budget and return if successful.
40
-
41
- Args:
42
- amount: Amount to consume
43
-
44
- Returns:
45
- True if budget was available and consumed
46
- """
47
- if amount <= self.remaining_budget:
48
- self.consumed_budget += amount
49
- self.remaining_budget -= amount
50
- self.last_update = datetime.now()
51
- return True
52
- return False
53
-
54
- def get_utilization_rate(self) -> float:
55
- """Get the budget utilization rate (0.0 to 1.0)."""
56
- if self.allocated_budget == 0:
57
- return 0.0
58
- return self.consumed_budget / self.allocated_budget
59
-
60
-
61
- @dataclass
62
- class BudgetSnapshot:
63
- """Snapshot of budget state at a point in time."""
64
-
65
- timestamp: datetime
66
- total_allocated: int
67
- total_consumed: int
68
- active_tasks: int
69
- task_allocations: Dict[str, BudgetAllocation]
70
-
71
-
72
- class BudgetManager:
73
- """Manages iteration budget allocation and tracking across tasks."""
74
-
75
- def __init__(
76
- self, total_budget: int = 100, min_task_budget: int = 2, reallocation_threshold: float = 0.2
77
- ):
78
- """Initialize the BudgetManager.
79
-
80
- Args:
81
- total_budget: Total iteration budget available
82
- min_task_budget: Minimum budget to allocate to any task
83
- reallocation_threshold: Utilization threshold for reallocation
84
- """
85
- self.total_budget = total_budget
86
- self.min_task_budget = min_task_budget
87
- self.reallocation_threshold = reallocation_threshold
88
-
89
- self._allocations: Dict[str, BudgetAllocation] = {}
90
- self._available_budget = total_budget
91
- self._allocation_history: List[BudgetSnapshot] = []
92
- self._reallocation_count = 0
93
-
94
- def allocate_budget(
95
- self,
96
- task_ids: List[str],
97
- complexity_scores: Optional[List[float]] = None,
98
- priorities: Optional[List[str]] = None,
99
- strategy: AllocationStrategy = AllocationStrategy.WEIGHTED,
100
- ) -> Dict[str, int]:
101
- """Allocate budget to multiple tasks based on strategy.
102
-
103
- Args:
104
- task_ids: List of task IDs to allocate budget to
105
- complexity_scores: Optional complexity scores (0.0-1.0) for each task
106
- priorities: Optional priority levels for each task
107
- strategy: Allocation strategy to use
108
-
109
- Returns:
110
- Dictionary mapping task IDs to allocated budgets
111
- """
112
- if not task_ids:
113
- return {}
114
-
115
- n_tasks = len(task_ids)
116
-
117
- # Determine allocation based on strategy
118
- if strategy == AllocationStrategy.EQUAL:
119
- allocations = self._allocate_equal(n_tasks)
120
- elif strategy == AllocationStrategy.WEIGHTED and complexity_scores:
121
- allocations = self._allocate_weighted(complexity_scores)
122
- elif strategy == AllocationStrategy.PRIORITY and priorities:
123
- allocations = self._allocate_by_priority(priorities)
124
- else:
125
- # Fallback to equal allocation
126
- allocations = self._allocate_equal(n_tasks)
127
-
128
- # Apply allocations
129
- result = {}
130
- for task_id, budget in zip(task_ids, allocations):
131
- if budget > 0:
132
- self._allocations[task_id] = BudgetAllocation(
133
- task_id=task_id, allocated_budget=budget
134
- )
135
- self._available_budget -= budget
136
- result[task_id] = budget
137
- logger.debug(f"Allocated {budget} iterations to task {task_id}")
138
-
139
- # Take snapshot
140
- self._take_snapshot()
141
-
142
- return result
143
-
144
- def consume_budget(self, task_id: str, amount: int = 1) -> bool:
145
- """Consume budget for a task.
146
-
147
- Args:
148
- task_id: Task ID
149
- amount: Amount to consume (default: 1)
150
-
151
- Returns:
152
- True if budget was available and consumed
153
- """
154
- if task_id not in self._allocations:
155
- logger.warning(f"No budget allocation found for task {task_id}")
156
- return False
157
-
158
- allocation = self._allocations[task_id]
159
- success = allocation.consume(amount)
160
-
161
- if not success:
162
- logger.warning(
163
- f"Insufficient budget for task {task_id}: "
164
- f"requested {amount}, available {allocation.remaining_budget}"
165
- )
166
-
167
- return success
168
-
169
- def get_remaining_budget(self, task_id: str) -> int:
170
- """Get remaining budget for a task.
171
-
172
- Args:
173
- task_id: Task ID
174
-
175
- Returns:
176
- Remaining budget or 0 if not found
177
- """
178
- if task_id in self._allocations:
179
- return self._allocations[task_id].remaining_budget
180
- return 0
181
-
182
- def get_utilization(self, task_id: Optional[str] = None) -> float:
183
- """Get budget utilization rate.
184
-
185
- Args:
186
- task_id: Optional task ID for specific utilization
187
-
188
- Returns:
189
- Utilization rate (0.0-1.0)
190
- """
191
- if task_id:
192
- if task_id in self._allocations:
193
- return self._allocations[task_id].get_utilization_rate()
194
- return 0.0
195
-
196
- # Overall utilization
197
- total_allocated = sum(a.allocated_budget for a in self._allocations.values())
198
- total_consumed = sum(a.consumed_budget for a in self._allocations.values())
199
-
200
- if total_allocated == 0:
201
- return 0.0
202
- return total_consumed / total_allocated
203
-
204
- def reallocate_unused_budget(self, force: bool = False) -> Dict[str, int]:
205
- """Reallocate unused budget from completed/failed tasks.
206
-
207
- Args:
208
- force: Force reallocation regardless of threshold
209
-
210
- Returns:
211
- Dictionary of new allocations made
212
- """
213
- # Find tasks with low utilization or completed tasks
214
- unused_budget = 0
215
- tasks_to_reclaim = []
216
-
217
- for task_id, allocation in self._allocations.items():
218
- utilization = allocation.get_utilization_rate()
219
-
220
- # Reclaim from tasks with very low utilization
221
- if utilization < self.reallocation_threshold or force:
222
- unused = allocation.remaining_budget
223
- if unused > 0:
224
- tasks_to_reclaim.append((task_id, unused))
225
- unused_budget += unused
226
-
227
- if unused_budget == 0:
228
- return {}
229
-
230
- # Find tasks that need more budget (high utilization)
231
- high_utilization_tasks = []
232
- for task_id, allocation in self._allocations.items():
233
- if task_id not in [t[0] for t in tasks_to_reclaim]:
234
- utilization = allocation.get_utilization_rate()
235
- if utilization > 0.8: # High utilization threshold
236
- high_utilization_tasks.append(task_id)
237
-
238
- if not high_utilization_tasks:
239
- # Return budget to available pool
240
- self._available_budget += unused_budget
241
- for task_id, amount in tasks_to_reclaim:
242
- self._allocations[task_id].allocated_budget -= amount
243
- self._allocations[task_id].remaining_budget = 0
244
- return {}
245
-
246
- # Reallocate to high utilization tasks
247
- per_task_addition = unused_budget // len(high_utilization_tasks)
248
- remainder = unused_budget % len(high_utilization_tasks)
249
-
250
- reallocations = {}
251
- for i, task_id in enumerate(high_utilization_tasks):
252
- additional = per_task_addition + (1 if i < remainder else 0)
253
- if additional > 0:
254
- self._allocations[task_id].allocated_budget += additional
255
- self._allocations[task_id].remaining_budget += additional
256
- reallocations[task_id] = additional
257
-
258
- # Update reclaimed tasks
259
- for task_id, amount in tasks_to_reclaim:
260
- self._allocations[task_id].allocated_budget -= amount
261
- self._allocations[task_id].remaining_budget = 0
262
-
263
- self._reallocation_count += 1
264
- logger.info(f"Reallocated {unused_budget} budget units to {len(reallocations)} tasks")
265
-
266
- return reallocations
267
-
268
- def release_task_budget(self, task_id: str) -> int:
269
- """Release all remaining budget for a task.
270
-
271
- Args:
272
- task_id: Task ID
273
-
274
- Returns:
275
- Amount of budget released
276
- """
277
- if task_id not in self._allocations:
278
- return 0
279
-
280
- allocation = self._allocations[task_id]
281
- released = allocation.remaining_budget
282
-
283
- if released > 0:
284
- self._available_budget += released
285
- allocation.remaining_budget = 0
286
- logger.debug(f"Released {released} budget units from task {task_id}")
287
-
288
- return released
289
-
290
- def _allocate_equal(self, n_tasks: int) -> List[int]:
291
- """Allocate budget equally among tasks.
292
-
293
- Args:
294
- n_tasks: Number of tasks
295
-
296
- Returns:
297
- List of budget allocations
298
- """
299
- if n_tasks == 0:
300
- return []
301
-
302
- available = min(self._available_budget, self.total_budget)
303
- per_task = max(self.min_task_budget, available // n_tasks)
304
-
305
- # Ensure we don't exceed available budget
306
- if per_task * n_tasks > available:
307
- per_task = available // n_tasks
308
-
309
- allocations = [per_task] * n_tasks
310
-
311
- # Distribute remainder
312
- remainder = available - (per_task * n_tasks)
313
- for i in range(min(remainder, n_tasks)):
314
- allocations[i] += 1
315
-
316
- return allocations
317
-
318
- def _allocate_weighted(self, complexity_scores: List[float]) -> List[int]:
319
- """Allocate budget based on complexity scores.
320
-
321
- Args:
322
- complexity_scores: List of complexity scores (0.0-1.0)
323
-
324
- Returns:
325
- List of budget allocations
326
- """
327
- if not complexity_scores:
328
- return []
329
-
330
- # Normalize scores
331
- total_score = sum(complexity_scores)
332
- if total_score == 0:
333
- return self._allocate_equal(len(complexity_scores))
334
-
335
- available = min(self._available_budget, self.total_budget)
336
- allocations = []
337
-
338
- for score in complexity_scores:
339
- # Weighted allocation with minimum guarantee
340
- weight = score / total_score
341
- allocation = max(self.min_task_budget, int(available * weight))
342
- allocations.append(allocation)
343
-
344
- # Adjust if we exceeded budget
345
- total_allocated = sum(allocations)
346
- if total_allocated > available:
347
- # Scale down proportionally
348
- scale = available / total_allocated
349
- allocations = [max(self.min_task_budget, int(a * scale)) for a in allocations]
350
-
351
- return allocations
352
-
353
- def _allocate_by_priority(self, priorities: List[str]) -> List[int]:
354
- """Allocate budget based on priority levels.
355
-
356
- Args:
357
- priorities: List of priority levels (high, medium, low)
358
-
359
- Returns:
360
- List of budget allocations
361
- """
362
- # Priority weights
363
- weights = {"high": 3, "medium": 2, "low": 1}
364
-
365
- # Convert to scores
366
- scores = [weights.get(p.lower(), 1) for p in priorities]
367
- total_weight = sum(scores)
368
-
369
- if total_weight == 0:
370
- return self._allocate_equal(len(priorities))
371
-
372
- # Convert to normalized complexity scores
373
- complexity_scores = [s / total_weight for s in scores]
374
-
375
- return self._allocate_weighted(complexity_scores)
376
-
377
- def _take_snapshot(self):
378
- """Take a snapshot of current budget state."""
379
- snapshot = BudgetSnapshot(
380
- timestamp=datetime.now(),
381
- total_allocated=sum(a.allocated_budget for a in self._allocations.values()),
382
- total_consumed=sum(a.consumed_budget for a in self._allocations.values()),
383
- active_tasks=len(self._allocations),
384
- task_allocations=self._allocations.copy(),
385
- )
386
- self._allocation_history.append(snapshot)
387
-
388
- def get_budget_summary(self) -> Dict[str, any]:
389
- """Get a summary of budget state.
390
-
391
- Returns:
392
- Dictionary with budget statistics
393
- """
394
- total_allocated = sum(a.allocated_budget for a in self._allocations.values())
395
- total_consumed = sum(a.consumed_budget for a in self._allocations.values())
396
-
397
- return {
398
- "total_budget": self.total_budget,
399
- "available_budget": self._available_budget,
400
- "allocated_budget": total_allocated,
401
- "consumed_budget": total_consumed,
402
- "utilization_rate": total_consumed / total_allocated if total_allocated > 0 else 0.0,
403
- "active_tasks": len(self._allocations),
404
- "reallocation_count": self._reallocation_count,
405
- "task_details": {
406
- task_id: {
407
- "allocated": alloc.allocated_budget,
408
- "consumed": alloc.consumed_budget,
409
- "remaining": alloc.remaining_budget,
410
- "utilization": alloc.get_utilization_rate(),
411
- }
412
- for task_id, alloc in self._allocations.items()
413
- },
414
- }