tunacode-cli 0.0.28__tar.gz → 0.0.29__tar.gz
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-0.0.28/src/tunacode_cli.egg-info → tunacode_cli-0.0.29}/PKG-INFO +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/pyproject.toml +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/configuration/defaults.py +1 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/constants.py +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/agents/main.py +83 -2
- tunacode_cli-0.0.29/src/tunacode/core/agents/orchestrator.py +213 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/agents/readonly.py +18 -4
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_fallback_responses.py +3 -1
- tunacode_cli-0.0.28/src/tunacode/core/agents/orchestrator.py +0 -117
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/CLAUDE.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/LICENSE +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/MANIFEST.in +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/README.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/TUNACODE.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/setup.cfg +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/commands.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/main.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/repl.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/textual_app.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/cli/textual_bridge.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/configuration/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/configuration/models.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/configuration/settings.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/context.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/agents/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/agents/planner_schema.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/background/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/background/manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/llm/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/llm/planner.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/agent_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/base.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/config_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/coordinator.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/environment_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/setup/git_safety_setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/state.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/core/tool_handler.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/exceptions.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/prompts/system.md +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/py.typed +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/services/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/services/mcp.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/setup.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/base.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/bash.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/grep.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/read_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/run_command.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/update_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/tools/write_file.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/types.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/completers.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/console.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/constants.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/decorators.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/input.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/keybindings.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/lexers.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/output.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/panels.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/prompt_manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/tool_ui.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/ui/validators.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/__init__.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/bm25.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/diff_utils.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/file_utils.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/import_cache.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/ripgrep.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/system.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/text_utils.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode/utils/user_configuration.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode_cli.egg-info/SOURCES.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode_cli.egg-info/requires.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/src/tunacode_cli.egg-info/top_level.txt +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_agent_initialization.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_architect_integration.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_architect_simple.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_background_manager.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_config_setup_async.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_fast_glob_search.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_file_reference_expansion.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_json_tool_parsing.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_orchestrator_file_references.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_orchestrator_import.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_orchestrator_planning_visibility.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_react_thoughts.py +0 -0
- {tunacode_cli-0.0.28 → tunacode_cli-0.0.29}/tests/test_update_command.py +0 -0
|
@@ -375,10 +375,91 @@ async def process_request(
|
|
|
375
375
|
if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
|
|
376
376
|
patch_tool_messages("Task incomplete", state_manager=state_manager)
|
|
377
377
|
response_state.has_final_synthesis = True
|
|
378
|
+
|
|
379
|
+
# Extract context from the agent run
|
|
380
|
+
tool_calls_summary = []
|
|
381
|
+
files_modified = set()
|
|
382
|
+
commands_run = []
|
|
383
|
+
|
|
384
|
+
# Analyze message history for context
|
|
385
|
+
for msg in state_manager.session.messages:
|
|
386
|
+
if hasattr(msg, "parts"):
|
|
387
|
+
for part in msg.parts:
|
|
388
|
+
if hasattr(part, "part_kind") and part.part_kind == "tool-call":
|
|
389
|
+
tool_name = getattr(part, "tool_name", "unknown")
|
|
390
|
+
tool_calls_summary.append(tool_name)
|
|
391
|
+
|
|
392
|
+
# Track specific operations
|
|
393
|
+
if tool_name in ["write_file", "update_file"] and hasattr(part, "args"):
|
|
394
|
+
if "file_path" in part.args:
|
|
395
|
+
files_modified.add(part.args["file_path"])
|
|
396
|
+
elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
|
|
397
|
+
if "command" in part.args:
|
|
398
|
+
commands_run.append(part.args["command"])
|
|
399
|
+
|
|
400
|
+
# Build fallback response with context
|
|
378
401
|
fallback = FallbackResponse(
|
|
379
402
|
summary="Reached maximum iterations without producing a final response.",
|
|
380
|
-
progress=f"{i}
|
|
403
|
+
progress=f"Completed {i} iterations (limit: {max_iterations})",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Get verbosity setting
|
|
407
|
+
verbosity = state_manager.session.user_config.get("settings", {}).get(
|
|
408
|
+
"fallback_verbosity", "normal"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if verbosity in ["normal", "detailed"]:
|
|
412
|
+
# Add what was attempted
|
|
413
|
+
if tool_calls_summary:
|
|
414
|
+
tool_counts = {}
|
|
415
|
+
for tool in tool_calls_summary:
|
|
416
|
+
tool_counts[tool] = tool_counts.get(tool, 0) + 1
|
|
417
|
+
|
|
418
|
+
fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
|
|
419
|
+
for tool, count in sorted(tool_counts.items()):
|
|
420
|
+
fallback.issues.append(f" • {tool}: {count}x")
|
|
421
|
+
|
|
422
|
+
if verbosity == "detailed":
|
|
423
|
+
if files_modified:
|
|
424
|
+
fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
|
|
425
|
+
for f in sorted(files_modified)[:5]: # Limit to 5 files
|
|
426
|
+
fallback.issues.append(f" • {f}")
|
|
427
|
+
if len(files_modified) > 5:
|
|
428
|
+
fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
|
|
429
|
+
|
|
430
|
+
if commands_run:
|
|
431
|
+
fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
|
|
432
|
+
for cmd in commands_run[:3]: # Limit to 3 commands
|
|
433
|
+
# Truncate long commands
|
|
434
|
+
display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
|
|
435
|
+
fallback.issues.append(f" • {display_cmd}")
|
|
436
|
+
if len(commands_run) > 3:
|
|
437
|
+
fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
|
|
438
|
+
|
|
439
|
+
# Add helpful next steps
|
|
440
|
+
fallback.next_steps.append(
|
|
441
|
+
"The task may be too complex - try breaking it into smaller steps"
|
|
381
442
|
)
|
|
443
|
+
fallback.next_steps.append("Check the output above for any errors or partial progress")
|
|
444
|
+
if files_modified:
|
|
445
|
+
fallback.next_steps.append("Review modified files to see what changes were made")
|
|
446
|
+
|
|
447
|
+
# Create comprehensive output
|
|
448
|
+
output_parts = [fallback.summary, ""]
|
|
449
|
+
|
|
450
|
+
if fallback.progress:
|
|
451
|
+
output_parts.append(f"Progress: {fallback.progress}")
|
|
452
|
+
|
|
453
|
+
if fallback.issues:
|
|
454
|
+
output_parts.append("\nWhat happened:")
|
|
455
|
+
output_parts.extend(fallback.issues)
|
|
456
|
+
|
|
457
|
+
if fallback.next_steps:
|
|
458
|
+
output_parts.append("\nSuggested next steps:")
|
|
459
|
+
for step in fallback.next_steps:
|
|
460
|
+
output_parts.append(f" • {step}")
|
|
461
|
+
|
|
462
|
+
comprehensive_output = "\n".join(output_parts)
|
|
382
463
|
|
|
383
464
|
# Create a wrapper object that mimics AgentRun with the required attributes
|
|
384
465
|
class AgentRunWrapper:
|
|
@@ -391,7 +472,7 @@ async def process_request(
|
|
|
391
472
|
# Delegate all other attributes to the wrapped object
|
|
392
473
|
return getattr(self._wrapped, name)
|
|
393
474
|
|
|
394
|
-
return AgentRunWrapper(agent_run, SimpleResult(
|
|
475
|
+
return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
|
|
395
476
|
|
|
396
477
|
# For non-fallback cases, we still need to handle the response_state
|
|
397
478
|
# Create a minimal wrapper just to add response_state
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Agent orchestration scaffolding.
|
|
2
|
+
|
|
3
|
+
This module defines an ``OrchestratorAgent`` class that demonstrates how
|
|
4
|
+
higher level planning and delegation could be layered on top of the existing
|
|
5
|
+
``process_request`` workflow. The goal is to keep orchestration logic isolated
|
|
6
|
+
from the core agent implementation while reusing all current tooling and state
|
|
7
|
+
handling provided by ``main.process_request``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import itertools
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
from ...types import AgentRun, FallbackResponse, ModelName, ResponseState
|
|
17
|
+
from ..llm.planner import make_plan
|
|
18
|
+
from ..state import StateManager
|
|
19
|
+
from . import main as agent_main
|
|
20
|
+
from .planner_schema import Task
|
|
21
|
+
from .readonly import ReadOnlyAgent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OrchestratorAgent:
|
|
25
|
+
"""Plan and run a sequence of sub-agent tasks."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, state_manager: StateManager):
|
|
28
|
+
self.state = state_manager
|
|
29
|
+
|
|
30
|
+
async def plan(self, request: str, model: ModelName) -> List[Task]:
|
|
31
|
+
"""Plan tasks for a user request using the planner LLM."""
|
|
32
|
+
|
|
33
|
+
return await make_plan(request, model, self.state)
|
|
34
|
+
|
|
35
|
+
async def _run_sub_task(self, task: Task, model: ModelName) -> AgentRun:
|
|
36
|
+
"""Execute a single task using an appropriate sub-agent."""
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
# Show which task is being executed
|
|
42
|
+
task_type = "WRITE" if task.mutate else "READ"
|
|
43
|
+
console.print(f"\n[dim][Task {task.id}] {task_type}[/dim]")
|
|
44
|
+
console.print(f"[dim] → {task.description}[/dim]")
|
|
45
|
+
|
|
46
|
+
if task.mutate:
|
|
47
|
+
agent_main.get_or_create_agent(model, self.state)
|
|
48
|
+
result = await agent_main.process_request(model, task.description, self.state)
|
|
49
|
+
else:
|
|
50
|
+
agent = ReadOnlyAgent(model, self.state)
|
|
51
|
+
result = await agent.process_request(task.description)
|
|
52
|
+
|
|
53
|
+
console.print(f"[dim][Task {task.id}] Complete[/dim]")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
async def run(self, request: str, model: ModelName | None = None) -> List[AgentRun]:
|
|
57
|
+
"""Plan and execute a user request.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
request:
|
|
62
|
+
The high level user request to process.
|
|
63
|
+
model:
|
|
64
|
+
Optional model name to use for sub agents. Defaults to the current
|
|
65
|
+
session model.
|
|
66
|
+
"""
|
|
67
|
+
from rich.console import Console
|
|
68
|
+
|
|
69
|
+
console = Console()
|
|
70
|
+
model = model or self.state.session.current_model
|
|
71
|
+
|
|
72
|
+
# Track response state across all sub-tasks
|
|
73
|
+
response_state = ResponseState()
|
|
74
|
+
|
|
75
|
+
# Show orchestrator is starting
|
|
76
|
+
console.print(
|
|
77
|
+
"\n[cyan]Orchestrator Mode: Analyzing request and creating execution plan...[/cyan]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
tasks = await self.plan(request, model)
|
|
81
|
+
|
|
82
|
+
# Show execution is starting
|
|
83
|
+
console.print(f"\n[cyan]Executing plan with {len(tasks)} tasks...[/cyan]")
|
|
84
|
+
|
|
85
|
+
results: List[AgentRun] = []
|
|
86
|
+
task_progress = []
|
|
87
|
+
|
|
88
|
+
for mutate_flag, group in itertools.groupby(tasks, key=lambda t: t.mutate):
|
|
89
|
+
if mutate_flag:
|
|
90
|
+
for t in group:
|
|
91
|
+
result = await self._run_sub_task(t, model)
|
|
92
|
+
results.append(result)
|
|
93
|
+
|
|
94
|
+
# Track task progress
|
|
95
|
+
task_progress.append(
|
|
96
|
+
{
|
|
97
|
+
"task": t,
|
|
98
|
+
"completed": True,
|
|
99
|
+
"had_output": hasattr(result, "result")
|
|
100
|
+
and result.result
|
|
101
|
+
and getattr(result.result, "output", None),
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Check if this task produced user-visible output
|
|
106
|
+
if hasattr(result, "response_state"):
|
|
107
|
+
response_state.has_user_response |= result.response_state.has_user_response
|
|
108
|
+
else:
|
|
109
|
+
# Show parallel execution
|
|
110
|
+
task_list = list(group)
|
|
111
|
+
if len(task_list) > 1:
|
|
112
|
+
console.print(
|
|
113
|
+
f"\n[dim][Parallel Execution] Running {len(task_list)} read-only tasks concurrently...[/dim]"
|
|
114
|
+
)
|
|
115
|
+
coros = [self._run_sub_task(t, model) for t in task_list]
|
|
116
|
+
parallel_results = await asyncio.gather(*coros)
|
|
117
|
+
results.extend(parallel_results)
|
|
118
|
+
|
|
119
|
+
# Track parallel task progress
|
|
120
|
+
for t, result in zip(task_list, parallel_results):
|
|
121
|
+
task_progress.append(
|
|
122
|
+
{
|
|
123
|
+
"task": t,
|
|
124
|
+
"completed": True,
|
|
125
|
+
"had_output": hasattr(result, "result")
|
|
126
|
+
and result.result
|
|
127
|
+
and getattr(result.result, "output", None),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Check if this task produced user-visible output
|
|
132
|
+
if hasattr(result, "response_state"):
|
|
133
|
+
response_state.has_user_response |= result.response_state.has_user_response
|
|
134
|
+
|
|
135
|
+
console.print("\n[green]Orchestrator completed all tasks successfully![/green]")
|
|
136
|
+
|
|
137
|
+
# Check if we need a fallback response
|
|
138
|
+
has_any_output = any(
|
|
139
|
+
hasattr(r, "result") and r.result and getattr(r.result, "output", None) for r in results
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
fallback_enabled = self.state.session.user_config.get("settings", {}).get(
|
|
143
|
+
"fallback_response", True
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Use has_any_output as the primary check since response_state might not be set for all agents
|
|
147
|
+
if not has_any_output and fallback_enabled:
|
|
148
|
+
# Generate a detailed fallback response
|
|
149
|
+
completed_count = sum(1 for tp in task_progress if tp["completed"])
|
|
150
|
+
output_count = sum(1 for tp in task_progress if tp["had_output"])
|
|
151
|
+
|
|
152
|
+
fallback = FallbackResponse(
|
|
153
|
+
summary="Orchestrator completed all tasks but no final response was generated.",
|
|
154
|
+
progress=f"Executed {completed_count}/{len(tasks)} tasks successfully",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Add task details based on verbosity
|
|
158
|
+
verbosity = self.state.session.user_config.get("settings", {}).get(
|
|
159
|
+
"fallback_verbosity", "normal"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if verbosity in ["normal", "detailed"]:
|
|
163
|
+
# List what was done
|
|
164
|
+
if task_progress:
|
|
165
|
+
fallback.issues.append(f"Tasks executed: {completed_count}")
|
|
166
|
+
if output_count == 0:
|
|
167
|
+
fallback.issues.append("No tasks produced visible output")
|
|
168
|
+
|
|
169
|
+
if verbosity == "detailed":
|
|
170
|
+
# Add task descriptions
|
|
171
|
+
for i, tp in enumerate(task_progress, 1):
|
|
172
|
+
task_type = "WRITE" if tp["task"].mutate else "READ"
|
|
173
|
+
status = "✓" if tp["completed"] else "✗"
|
|
174
|
+
fallback.issues.append(
|
|
175
|
+
f"{status} Task {i} [{task_type}]: {tp['task'].description}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Suggest next steps
|
|
179
|
+
fallback.next_steps.append("Review the task execution above for any errors")
|
|
180
|
+
fallback.next_steps.append(
|
|
181
|
+
"Try running individual tasks separately for more detailed output"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Create synthesized response
|
|
185
|
+
synthesis_parts = [fallback.summary, ""]
|
|
186
|
+
|
|
187
|
+
if fallback.progress:
|
|
188
|
+
synthesis_parts.append(f"Progress: {fallback.progress}")
|
|
189
|
+
|
|
190
|
+
if fallback.issues:
|
|
191
|
+
synthesis_parts.append("\nDetails:")
|
|
192
|
+
synthesis_parts.extend(f" • {issue}" for issue in fallback.issues)
|
|
193
|
+
|
|
194
|
+
if fallback.next_steps:
|
|
195
|
+
synthesis_parts.append("\nNext steps:")
|
|
196
|
+
synthesis_parts.extend(f" • {step}" for step in fallback.next_steps)
|
|
197
|
+
|
|
198
|
+
synthesis = "\n".join(synthesis_parts)
|
|
199
|
+
|
|
200
|
+
class FallbackResult:
|
|
201
|
+
def __init__(self, output: str, response_state: ResponseState):
|
|
202
|
+
self.output = output
|
|
203
|
+
self.response_state = response_state
|
|
204
|
+
|
|
205
|
+
class FallbackRun:
|
|
206
|
+
def __init__(self, synthesis: str, response_state: ResponseState):
|
|
207
|
+
self.result = FallbackResult(synthesis, response_state)
|
|
208
|
+
self.response_state = response_state
|
|
209
|
+
|
|
210
|
+
response_state.has_final_synthesis = True
|
|
211
|
+
results.append(FallbackRun(synthesis, response_state))
|
|
212
|
+
|
|
213
|
+
return results
|
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
from ...tools.grep import grep
|
|
8
8
|
from ...tools.read_file import read_file
|
|
9
|
-
from ...types import AgentRun, ModelName
|
|
9
|
+
from ...types import AgentRun, ModelName, ResponseState
|
|
10
10
|
from ..state import StateManager
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -42,10 +42,24 @@ class ReadOnlyAgent:
|
|
|
42
42
|
async def process_request(self, request: str) -> AgentRun:
|
|
43
43
|
"""Process a request using only read-only tools."""
|
|
44
44
|
agent = self._get_agent()
|
|
45
|
+
response_state = ResponseState()
|
|
45
46
|
|
|
46
47
|
# Use iter() like main.py does to get the full run context
|
|
47
48
|
async with agent.iter(request) as agent_run:
|
|
48
|
-
async for
|
|
49
|
-
|
|
49
|
+
async for node in agent_run:
|
|
50
|
+
# Check if this node produced user-visible output
|
|
51
|
+
if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
|
|
52
|
+
if node.result.output:
|
|
53
|
+
response_state.has_user_response = True
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
# Wrap the agent run to include response_state
|
|
56
|
+
class AgentRunWithState:
|
|
57
|
+
def __init__(self, wrapped_run):
|
|
58
|
+
self._wrapped = wrapped_run
|
|
59
|
+
self.response_state = response_state
|
|
60
|
+
|
|
61
|
+
def __getattr__(self, name):
|
|
62
|
+
# Delegate all other attributes to the wrapped object
|
|
63
|
+
return getattr(self._wrapped, name)
|
|
64
|
+
|
|
65
|
+
return AgentRunWithState(agent_run)
|
|
@@ -51,4 +51,6 @@ async def test_orchestrator_synthesizes_summary():
|
|
|
51
51
|
with patch.object(orch, "_run_sub_task", return_value=fake_run):
|
|
52
52
|
results = await orch.run("req")
|
|
53
53
|
assert len(results) == 2
|
|
54
|
-
|
|
54
|
+
# Check that fallback response was generated
|
|
55
|
+
assert "orchestrator completed all tasks" in results[-1].result.output.lower()
|
|
56
|
+
assert "executed 1/1 tasks" in results[-1].result.output.lower()
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"""Agent orchestration scaffolding.
|
|
2
|
-
|
|
3
|
-
This module defines an ``OrchestratorAgent`` class that demonstrates how
|
|
4
|
-
higher level planning and delegation could be layered on top of the existing
|
|
5
|
-
``process_request`` workflow. The goal is to keep orchestration logic isolated
|
|
6
|
-
from the core agent implementation while reusing all current tooling and state
|
|
7
|
-
handling provided by ``main.process_request``.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import itertools
|
|
14
|
-
from typing import List
|
|
15
|
-
|
|
16
|
-
from ...types import AgentRun, ModelName
|
|
17
|
-
from ..llm.planner import make_plan
|
|
18
|
-
from ..state import StateManager
|
|
19
|
-
from . import main as agent_main
|
|
20
|
-
from .planner_schema import Task
|
|
21
|
-
from .readonly import ReadOnlyAgent
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class OrchestratorAgent:
|
|
25
|
-
"""Plan and run a sequence of sub-agent tasks."""
|
|
26
|
-
|
|
27
|
-
def __init__(self, state_manager: StateManager):
|
|
28
|
-
self.state = state_manager
|
|
29
|
-
|
|
30
|
-
async def plan(self, request: str, model: ModelName) -> List[Task]:
|
|
31
|
-
"""Plan tasks for a user request using the planner LLM."""
|
|
32
|
-
|
|
33
|
-
return await make_plan(request, model, self.state)
|
|
34
|
-
|
|
35
|
-
async def _run_sub_task(self, task: Task, model: ModelName) -> AgentRun:
|
|
36
|
-
"""Execute a single task using an appropriate sub-agent."""
|
|
37
|
-
from rich.console import Console
|
|
38
|
-
|
|
39
|
-
console = Console()
|
|
40
|
-
|
|
41
|
-
# Show which task is being executed
|
|
42
|
-
task_type = "WRITE" if task.mutate else "READ"
|
|
43
|
-
console.print(f"\n[dim][Task {task.id}] {task_type}[/dim]")
|
|
44
|
-
console.print(f"[dim] → {task.description}[/dim]")
|
|
45
|
-
|
|
46
|
-
if task.mutate:
|
|
47
|
-
agent_main.get_or_create_agent(model, self.state)
|
|
48
|
-
result = await agent_main.process_request(model, task.description, self.state)
|
|
49
|
-
else:
|
|
50
|
-
agent = ReadOnlyAgent(model, self.state)
|
|
51
|
-
result = await agent.process_request(task.description)
|
|
52
|
-
|
|
53
|
-
console.print(f"[dim][Task {task.id}] Complete[/dim]")
|
|
54
|
-
return result
|
|
55
|
-
|
|
56
|
-
async def run(self, request: str, model: ModelName | None = None) -> List[AgentRun]:
|
|
57
|
-
"""Plan and execute a user request.
|
|
58
|
-
|
|
59
|
-
Parameters
|
|
60
|
-
----------
|
|
61
|
-
request:
|
|
62
|
-
The high level user request to process.
|
|
63
|
-
model:
|
|
64
|
-
Optional model name to use for sub agents. Defaults to the current
|
|
65
|
-
session model.
|
|
66
|
-
"""
|
|
67
|
-
from rich.console import Console
|
|
68
|
-
|
|
69
|
-
console = Console()
|
|
70
|
-
model = model or self.state.session.current_model
|
|
71
|
-
|
|
72
|
-
# Show orchestrator is starting
|
|
73
|
-
console.print(
|
|
74
|
-
"\n[cyan]Orchestrator Mode: Analyzing request and creating execution plan...[/cyan]"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
tasks = await self.plan(request, model)
|
|
78
|
-
|
|
79
|
-
# Show execution is starting
|
|
80
|
-
console.print(f"\n[cyan]Executing plan with {len(tasks)} tasks...[/cyan]")
|
|
81
|
-
|
|
82
|
-
results: List[AgentRun] = []
|
|
83
|
-
for mutate_flag, group in itertools.groupby(tasks, key=lambda t: t.mutate):
|
|
84
|
-
if mutate_flag:
|
|
85
|
-
for t in group:
|
|
86
|
-
results.append(await self._run_sub_task(t, model))
|
|
87
|
-
else:
|
|
88
|
-
# Show parallel execution
|
|
89
|
-
task_list = list(group)
|
|
90
|
-
if len(task_list) > 1:
|
|
91
|
-
console.print(
|
|
92
|
-
f"\n[dim][Parallel Execution] Running {len(task_list)} read-only tasks concurrently...[/dim]"
|
|
93
|
-
)
|
|
94
|
-
coros = [self._run_sub_task(t, model) for t in task_list]
|
|
95
|
-
results.extend(await asyncio.gather(*coros))
|
|
96
|
-
|
|
97
|
-
console.print("\n[green]Orchestrator completed all tasks successfully![/green]")
|
|
98
|
-
|
|
99
|
-
has_output = any(
|
|
100
|
-
hasattr(r, "result") and r.result and getattr(r.result, "output", None) for r in results
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if results and not has_output:
|
|
104
|
-
lines = [f"Task {i + 1} completed" for i in range(len(results))]
|
|
105
|
-
summary = "\n".join(lines)
|
|
106
|
-
|
|
107
|
-
class SynthResult:
|
|
108
|
-
def __init__(self, output: str):
|
|
109
|
-
self.output = output
|
|
110
|
-
|
|
111
|
-
class SynthRun:
|
|
112
|
-
def __init__(self):
|
|
113
|
-
self.result = SynthResult(summary)
|
|
114
|
-
|
|
115
|
-
results.append(SynthRun())
|
|
116
|
-
|
|
117
|
-
return results
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|