tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/__init__.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Agent composition utilities for multi-agent delegation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
from tsugite.core.tools import Tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_agent_reference(ref: str, base_dir: Path) -> Path:
|
|
10
|
+
"""Resolve agent reference to file path.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
ref: Agent reference (path or +name shorthand)
|
|
14
|
+
base_dir: Base directory for relative path resolution
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Resolved Path to agent file
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If agent not found or invalid
|
|
21
|
+
"""
|
|
22
|
+
# Handle +name shorthand
|
|
23
|
+
if ref.startswith("+"):
|
|
24
|
+
agent_name = ref[1:]
|
|
25
|
+
from .agent_inheritance import find_agent_file
|
|
26
|
+
|
|
27
|
+
found = find_agent_file(agent_name, base_dir)
|
|
28
|
+
if not found:
|
|
29
|
+
raise ValueError(f"Agent not found: {ref} (searched standard locations)")
|
|
30
|
+
return found
|
|
31
|
+
|
|
32
|
+
# Handle regular path
|
|
33
|
+
agent_path = Path(ref)
|
|
34
|
+
if not agent_path.is_absolute():
|
|
35
|
+
agent_path = base_dir / agent_path
|
|
36
|
+
|
|
37
|
+
if agent_path.exists():
|
|
38
|
+
return agent_path
|
|
39
|
+
|
|
40
|
+
# If path doesn't exist, try as agent name lookup
|
|
41
|
+
# This allows "helper" or "helper.md" to work like "+helper"
|
|
42
|
+
from .agent_inheritance import find_agent_file
|
|
43
|
+
|
|
44
|
+
found = find_agent_file(ref, base_dir)
|
|
45
|
+
if found:
|
|
46
|
+
return found
|
|
47
|
+
|
|
48
|
+
raise ValueError(f"Agent file not found: {ref}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_delegation_tool(agent_name: str, agent_path: Path) -> Tool:
|
|
52
|
+
"""Create a delegation tool for a specific agent.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
agent_name: Name for the tool (e.g., "jira" -> spawn_jira)
|
|
56
|
+
agent_path: Path to the agent file
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tool that wraps spawn_agent with pre-filled path
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def delegation_function(prompt: str) -> str:
|
|
63
|
+
"""Execute the delegated agent.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
prompt: Task/prompt to give the agent
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: Result from the delegated agent
|
|
70
|
+
"""
|
|
71
|
+
from .agent_runner import run_agent
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
result = run_agent(
|
|
75
|
+
agent_path=agent_path,
|
|
76
|
+
prompt=prompt,
|
|
77
|
+
context={},
|
|
78
|
+
model_override=None,
|
|
79
|
+
debug=False,
|
|
80
|
+
)
|
|
81
|
+
return str(result)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return f"Error executing {agent_name} agent: {e}"
|
|
84
|
+
|
|
85
|
+
# Create Tool instance with proper schema
|
|
86
|
+
return Tool(
|
|
87
|
+
name=f"spawn_{agent_name}",
|
|
88
|
+
description=f"Delegate a task to the {agent_name} agent and get the result.",
|
|
89
|
+
parameters={
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {"prompt": {"type": "string", "description": "Task/prompt to give the agent"}},
|
|
92
|
+
"required": ["prompt"],
|
|
93
|
+
},
|
|
94
|
+
function=delegation_function,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_delegation_tools(delegation_agents: List[Tuple[str, Path]]) -> List[Tool]:
|
|
99
|
+
"""Create delegation tools for multiple agents.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
delegation_agents: List of (agent_name, agent_path) tuples
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of delegation tools
|
|
106
|
+
"""
|
|
107
|
+
tools = []
|
|
108
|
+
for agent_name, agent_path in delegation_agents:
|
|
109
|
+
tool = create_delegation_tool(agent_name, agent_path)
|
|
110
|
+
tools.append(tool)
|
|
111
|
+
return tools
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_agent_references(
|
|
115
|
+
agent_refs: List[str], with_agents_str: str | None, base_dir: Path
|
|
116
|
+
) -> Tuple[Path, List[Tuple[str, Path]]]:
|
|
117
|
+
"""Parse agent references into primary agent and delegation agents.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
agent_refs: List of agent references from CLI args
|
|
121
|
+
with_agents_str: Comma or space separated string from --with-agents option
|
|
122
|
+
base_dir: Base directory for resolution
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (primary_agent_path, [(name, path), ...])
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If no agents specified or resolution fails
|
|
129
|
+
"""
|
|
130
|
+
if not agent_refs:
|
|
131
|
+
raise ValueError("No agent specified")
|
|
132
|
+
|
|
133
|
+
# Resolve primary agent (first in list)
|
|
134
|
+
primary_ref = agent_refs[0]
|
|
135
|
+
primary_path = resolve_agent_reference(primary_ref, base_dir)
|
|
136
|
+
|
|
137
|
+
# Collect delegation agent references
|
|
138
|
+
delegation_refs = []
|
|
139
|
+
|
|
140
|
+
# Add remaining positional agents
|
|
141
|
+
if len(agent_refs) > 1:
|
|
142
|
+
delegation_refs.extend(agent_refs[1:])
|
|
143
|
+
|
|
144
|
+
# Add --with-agents if provided
|
|
145
|
+
if with_agents_str:
|
|
146
|
+
# Support both comma and space separated
|
|
147
|
+
import re
|
|
148
|
+
|
|
149
|
+
refs = re.split(r"[,\s]+", with_agents_str.strip())
|
|
150
|
+
delegation_refs.extend([ref for ref in refs if ref])
|
|
151
|
+
|
|
152
|
+
# Resolve delegation agents
|
|
153
|
+
delegation_agents = []
|
|
154
|
+
for ref in delegation_refs:
|
|
155
|
+
agent_path = resolve_agent_reference(ref, base_dir)
|
|
156
|
+
# Extract name from reference (strip + if present, remove .md extension)
|
|
157
|
+
agent_name = ref.lstrip("+").removesuffix(".md")
|
|
158
|
+
# If it's a path, use the filename without extension
|
|
159
|
+
if "/" in agent_name:
|
|
160
|
+
agent_name = Path(agent_name).stem
|
|
161
|
+
delegation_agents.append((agent_name, agent_path))
|
|
162
|
+
|
|
163
|
+
return primary_path, delegation_agents
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""Agent inheritance resolution for Tsugite."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
from tsugite.utils import ensure_file_exists
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_builtin_agents_path() -> Path:
|
|
11
|
+
"""Get the built-in agents directory path.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Path to built-in agents directory within the package
|
|
15
|
+
"""
|
|
16
|
+
return Path(__file__).parent / "builtin_agents"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_global_agents_paths() -> List[Path]:
|
|
20
|
+
"""Get global agent directory paths in precedence order.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of paths to check for global agents (XDG-compliant)
|
|
24
|
+
"""
|
|
25
|
+
paths = []
|
|
26
|
+
|
|
27
|
+
# ~/.tsugite/agents/
|
|
28
|
+
home_tsugite = Path.home() / ".tsugite" / "agents"
|
|
29
|
+
paths.append(home_tsugite)
|
|
30
|
+
|
|
31
|
+
# $XDG_CONFIG_HOME/tsugite/agents/
|
|
32
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
|
33
|
+
if xdg_config:
|
|
34
|
+
xdg_path = Path(xdg_config) / "tsugite" / "agents"
|
|
35
|
+
paths.append(xdg_path)
|
|
36
|
+
|
|
37
|
+
# ~/.config/tsugite/agents/
|
|
38
|
+
default_path = Path.home() / ".config" / "tsugite" / "agents"
|
|
39
|
+
paths.append(default_path)
|
|
40
|
+
|
|
41
|
+
return paths
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_agent_file(agent_ref: str, current_agent_dir: Path) -> Optional[Path]:
|
|
45
|
+
"""Find an agent file by reference.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
agent_ref: Agent reference (name or path)
|
|
49
|
+
current_agent_dir: Directory of the agent doing the extending
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Path to agent file if found, None otherwise
|
|
53
|
+
|
|
54
|
+
Search order:
|
|
55
|
+
1. If path-like (contains / or .md), resolve relative to current agent
|
|
56
|
+
2. .tsugite/{name}.md (project-local shared)
|
|
57
|
+
3. ./agents/{name}.md (project convention)
|
|
58
|
+
4. ./{name}.md (current directory)
|
|
59
|
+
5. Built-in agents directory (tsugite/builtin_agents/)
|
|
60
|
+
6. Global agent directories (XDG order)
|
|
61
|
+
"""
|
|
62
|
+
# If it looks like a path, resolve it relative to current agent
|
|
63
|
+
if "/" in agent_ref or agent_ref.endswith(".md"):
|
|
64
|
+
path = current_agent_dir / agent_ref
|
|
65
|
+
if path.exists():
|
|
66
|
+
return path.resolve()
|
|
67
|
+
# Also try as absolute path
|
|
68
|
+
abs_path = Path(agent_ref).expanduser()
|
|
69
|
+
if abs_path.exists():
|
|
70
|
+
return abs_path.resolve()
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Ensure .md extension for name-based lookup
|
|
74
|
+
agent_name = agent_ref if agent_ref.endswith(".md") else f"{agent_ref}.md"
|
|
75
|
+
|
|
76
|
+
# Search project-local locations
|
|
77
|
+
search_paths = [
|
|
78
|
+
current_agent_dir / ".tsugite" / agent_name,
|
|
79
|
+
current_agent_dir / "agents" / agent_name,
|
|
80
|
+
current_agent_dir / agent_name,
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Add built-in agents directory
|
|
84
|
+
builtin_path = get_builtin_agents_path() / agent_name
|
|
85
|
+
search_paths.append(builtin_path)
|
|
86
|
+
|
|
87
|
+
# Add global locations
|
|
88
|
+
for global_dir in get_global_agents_paths():
|
|
89
|
+
search_paths.append(global_dir / agent_name)
|
|
90
|
+
|
|
91
|
+
# Return first existing path
|
|
92
|
+
for path in search_paths:
|
|
93
|
+
if path.exists():
|
|
94
|
+
return path.resolve()
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def detect_circular_inheritance(agent_path: Path, inheritance_chain: Set[Path]) -> bool:
|
|
100
|
+
"""Detect circular inheritance.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent_path: Path of agent being loaded
|
|
104
|
+
inheritance_chain: Set of agent paths already in the chain
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if circular inheritance detected, False otherwise
|
|
108
|
+
"""
|
|
109
|
+
resolved_path = agent_path.resolve()
|
|
110
|
+
return resolved_path in inheritance_chain
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_extended_agent(extends_ref: str, current_agent_path: Path, inheritance_chain: Set[Path]):
|
|
114
|
+
"""Load an extended (parent) agent.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
extends_ref: Reference to parent agent
|
|
118
|
+
current_agent_path: Path to current agent file
|
|
119
|
+
inheritance_chain: Set of agent paths in current inheritance chain
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Parsed Agent object
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: If agent not found or circular inheritance detected
|
|
126
|
+
"""
|
|
127
|
+
from .md_agents import parse_agent
|
|
128
|
+
|
|
129
|
+
current_agent_dir = current_agent_path.parent if current_agent_path.is_file() else current_agent_path
|
|
130
|
+
|
|
131
|
+
agent_path = find_agent_file(extends_ref, current_agent_dir)
|
|
132
|
+
|
|
133
|
+
if agent_path is None:
|
|
134
|
+
raise ValueError(f"Extended agent not found: {extends_ref}")
|
|
135
|
+
|
|
136
|
+
# Check for circular inheritance
|
|
137
|
+
if detect_circular_inheritance(agent_path, inheritance_chain):
|
|
138
|
+
chain_str = " -> ".join(str(p) for p in inheritance_chain)
|
|
139
|
+
raise ValueError(f"Circular inheritance detected: {chain_str} -> {agent_path}")
|
|
140
|
+
|
|
141
|
+
# Load the parent agent WITHOUT resolving its inheritance yet
|
|
142
|
+
# (we'll do that recursively in resolve_agent_inheritance)
|
|
143
|
+
ensure_file_exists(agent_path, "Agent file")
|
|
144
|
+
|
|
145
|
+
content = agent_path.read_text(encoding="utf-8")
|
|
146
|
+
return parse_agent(content, agent_path)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def merge_agent_configs(parent, child):
|
|
150
|
+
"""Merge parent and child agent configurations.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
parent: Parent AgentConfig
|
|
154
|
+
child: Child AgentConfig
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Merged AgentConfig with child taking precedence
|
|
158
|
+
|
|
159
|
+
Merge rules:
|
|
160
|
+
- Scalars (model, max_turns, reasoning_effort, text_mode, etc.): child overwrites parent
|
|
161
|
+
- Lists (tools, attachments): merge and deduplicate
|
|
162
|
+
- Lists (prefetch): concatenate (parent first, no deduplication)
|
|
163
|
+
- Lists of dicts (custom_tools): merge and deduplicate by "name" field
|
|
164
|
+
- Dicts (mcp_servers, context_budget): merge, child keys override
|
|
165
|
+
- Strings (instructions): concatenate with newline
|
|
166
|
+
"""
|
|
167
|
+
from .md_agents import AgentConfig
|
|
168
|
+
|
|
169
|
+
# Merge all field types
|
|
170
|
+
merged_data = {}
|
|
171
|
+
merged_data.update(merge_scalar_fields(parent, child))
|
|
172
|
+
merged_data.update(merge_list_fields(parent, child))
|
|
173
|
+
merged_data.update(merge_dict_fields(parent, child))
|
|
174
|
+
merged_data["instructions"] = merge_instructions(parent, child)
|
|
175
|
+
|
|
176
|
+
return AgentConfig(**merged_data)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_agent_inheritance(agent, agent_path: Path, inheritance_chain: Optional[Set[Path]] = None):
|
|
180
|
+
"""Resolve agent inheritance chain.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
agent: Parsed Agent object
|
|
184
|
+
agent_path: Path to agent file
|
|
185
|
+
inheritance_chain: Set of agent paths already processed (for cycle detection)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Agent object with resolved inheritance
|
|
189
|
+
|
|
190
|
+
Inheritance chain:
|
|
191
|
+
1. Default base agent (from config, usually "default")
|
|
192
|
+
2. Extended agent (if extends specified)
|
|
193
|
+
3. Current agent
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If circular inheritance or missing agent
|
|
197
|
+
"""
|
|
198
|
+
from .md_agents import Agent
|
|
199
|
+
|
|
200
|
+
if inheritance_chain is None:
|
|
201
|
+
inheritance_chain = set()
|
|
202
|
+
|
|
203
|
+
# Add current agent to chain
|
|
204
|
+
resolved_path = agent_path.resolve()
|
|
205
|
+
inheritance_chain.add(resolved_path)
|
|
206
|
+
|
|
207
|
+
# Check if explicitly opted out
|
|
208
|
+
if agent.config.extends == "none":
|
|
209
|
+
return agent
|
|
210
|
+
|
|
211
|
+
# Build inheritance chain
|
|
212
|
+
configs_to_merge = []
|
|
213
|
+
contents_to_merge = []
|
|
214
|
+
|
|
215
|
+
# 1. Load default base agent (only for implicit inheritance)
|
|
216
|
+
if agent.config.extends is None:
|
|
217
|
+
default_config, default_content = load_default_base_agent(agent_path, resolved_path, inheritance_chain)
|
|
218
|
+
if default_config:
|
|
219
|
+
configs_to_merge.append(default_config)
|
|
220
|
+
contents_to_merge.append(default_content)
|
|
221
|
+
|
|
222
|
+
# 2. Load explicitly extended agent
|
|
223
|
+
if agent.config.extends and agent.config.extends != "none":
|
|
224
|
+
parent_config, parent_content = load_explicit_parent_agent(agent.config.extends, agent_path, inheritance_chain)
|
|
225
|
+
configs_to_merge.append(parent_config)
|
|
226
|
+
contents_to_merge.append(parent_content)
|
|
227
|
+
|
|
228
|
+
# 3. Current agent is last (highest precedence)
|
|
229
|
+
configs_to_merge.append(agent.config)
|
|
230
|
+
contents_to_merge.append(agent.content)
|
|
231
|
+
|
|
232
|
+
# Merge all configs and content
|
|
233
|
+
if len(configs_to_merge) == 1:
|
|
234
|
+
return agent
|
|
235
|
+
|
|
236
|
+
merged_config, merged_content = merge_configs_and_content(configs_to_merge, contents_to_merge)
|
|
237
|
+
return Agent(config=merged_config, content=merged_content, file_path=agent_path)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _get_default_base_agent_name() -> Optional[str]:
|
|
241
|
+
"""Get the default base agent name from config.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Default base agent name, or "default" as fallback, or None if disabled
|
|
245
|
+
"""
|
|
246
|
+
from .config import load_config
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
config = load_config()
|
|
250
|
+
if config.default_base_agent is None:
|
|
251
|
+
return "default"
|
|
252
|
+
return config.default_base_agent
|
|
253
|
+
except Exception:
|
|
254
|
+
return "default"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def merge_scalar_fields(parent, child) -> Dict[str, Any]:
|
|
258
|
+
"""Merge scalar fields from parent and child configs.
|
|
259
|
+
|
|
260
|
+
Child values take precedence when explicitly set.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
parent: Parent AgentConfig
|
|
264
|
+
child: Child AgentConfig
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dict of merged scalar fields
|
|
268
|
+
"""
|
|
269
|
+
return {
|
|
270
|
+
"name": child.name if child.name else parent.name,
|
|
271
|
+
"description": child.description if child.description else parent.description,
|
|
272
|
+
"model": child.model if child.model else parent.model,
|
|
273
|
+
"max_turns": child.max_turns if child.max_turns != 5 else parent.max_turns,
|
|
274
|
+
"permissions_profile": (
|
|
275
|
+
child.permissions_profile if child.permissions_profile != "default" else parent.permissions_profile
|
|
276
|
+
),
|
|
277
|
+
"reasoning_effort": child.reasoning_effort if child.reasoning_effort else parent.reasoning_effort,
|
|
278
|
+
"text_mode": child.text_mode if child.text_mode else parent.text_mode,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def merge_list_fields(parent, child) -> Dict[str, List]:
|
|
283
|
+
"""Merge list fields from parent and child configs.
|
|
284
|
+
|
|
285
|
+
Different list types have different merge strategies:
|
|
286
|
+
- tools, attachments: merge and deduplicate
|
|
287
|
+
- prefetch, initial_tasks: concatenate (parent first)
|
|
288
|
+
- custom_tools: deduplicate by "name" field
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
parent: Parent AgentConfig
|
|
292
|
+
child: Child AgentConfig
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dict of merged list fields
|
|
296
|
+
"""
|
|
297
|
+
# Simple lists - merge and deduplicate while preserving order
|
|
298
|
+
parent_tools = parent.tools if parent.tools else []
|
|
299
|
+
child_tools = child.tools if child.tools else []
|
|
300
|
+
merged_tools = list(dict.fromkeys(parent_tools + child_tools))
|
|
301
|
+
|
|
302
|
+
parent_attachments = parent.attachments if parent.attachments else []
|
|
303
|
+
child_attachments = child.attachments if child.attachments else []
|
|
304
|
+
merged_attachments = list(dict.fromkeys(parent_attachments + child_attachments))
|
|
305
|
+
|
|
306
|
+
# Lists that concatenate without deduplication
|
|
307
|
+
parent_prefetch = parent.prefetch if parent.prefetch else []
|
|
308
|
+
child_prefetch = child.prefetch if child.prefetch else []
|
|
309
|
+
|
|
310
|
+
parent_initial_tasks = parent.initial_tasks if parent.initial_tasks else []
|
|
311
|
+
child_initial_tasks = child.initial_tasks if child.initial_tasks else []
|
|
312
|
+
|
|
313
|
+
# Custom tools - deduplicate by "name" field (child overrides parent)
|
|
314
|
+
parent_custom = parent.custom_tools if parent.custom_tools else []
|
|
315
|
+
child_custom = child.custom_tools if child.custom_tools else []
|
|
316
|
+
custom_tool_dict = {}
|
|
317
|
+
for tool in parent_custom + child_custom:
|
|
318
|
+
custom_tool_dict[tool["name"]] = tool
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"tools": merged_tools,
|
|
322
|
+
"attachments": merged_attachments,
|
|
323
|
+
"prefetch": parent_prefetch + child_prefetch,
|
|
324
|
+
"initial_tasks": parent_initial_tasks + child_initial_tasks,
|
|
325
|
+
"custom_tools": list(custom_tool_dict.values()),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def merge_dict_fields(parent, child) -> Dict[str, Any]:
|
|
330
|
+
"""Merge dictionary fields from parent and child configs.
|
|
331
|
+
|
|
332
|
+
Child keys override parent keys.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
parent: Parent AgentConfig
|
|
336
|
+
child: Child AgentConfig
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict of merged dict fields
|
|
340
|
+
"""
|
|
341
|
+
# MCP servers
|
|
342
|
+
parent_mcp = parent.mcp_servers if parent.mcp_servers else {}
|
|
343
|
+
child_mcp = child.mcp_servers if child.mcp_servers else {}
|
|
344
|
+
merged_mcp = {**parent_mcp, **child_mcp}
|
|
345
|
+
|
|
346
|
+
# Context budget
|
|
347
|
+
parent_context = parent.context_budget if parent.context_budget else {}
|
|
348
|
+
child_context = child.context_budget if child.context_budget else {}
|
|
349
|
+
merged_context = {**parent_context, **child_context}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"mcp_servers": merged_mcp,
|
|
353
|
+
"context_budget": merged_context,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def merge_instructions(parent, child) -> str:
|
|
358
|
+
"""Merge instructions from parent and child configs.
|
|
359
|
+
|
|
360
|
+
Concatenates with double newline separator.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
parent: Parent AgentConfig
|
|
364
|
+
child: Child AgentConfig
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Merged instructions string
|
|
368
|
+
"""
|
|
369
|
+
parent_instructions = parent.instructions if parent.instructions else ""
|
|
370
|
+
child_instructions = child.instructions if child.instructions else ""
|
|
371
|
+
|
|
372
|
+
if parent_instructions and child_instructions:
|
|
373
|
+
return f"{parent_instructions}\n\n{child_instructions}"
|
|
374
|
+
elif parent_instructions:
|
|
375
|
+
return parent_instructions
|
|
376
|
+
elif child_instructions:
|
|
377
|
+
return child_instructions
|
|
378
|
+
else:
|
|
379
|
+
return ""
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def load_default_base_agent(
|
|
383
|
+
agent_path: Path, resolved_path: Path, inheritance_chain: Set[Path]
|
|
384
|
+
) -> tuple[Optional[Any], Optional[str]]:
|
|
385
|
+
"""Load default base agent for implicit inheritance.
|
|
386
|
+
|
|
387
|
+
Search priority:
|
|
388
|
+
1. User's project-local default.md (if exists)
|
|
389
|
+
2. Config's default_base_agent
|
|
390
|
+
3. Builtin fallback
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
agent_path: Path to current agent file
|
|
394
|
+
resolved_path: Resolved path to current agent
|
|
395
|
+
inheritance_chain: Set of agent paths already in chain
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (loaded_agent, loaded_content) or (None, None) if not loaded
|
|
399
|
+
"""
|
|
400
|
+
# Try project-local default.md first
|
|
401
|
+
user_default_path = agent_path.parent / ".tsugite" / "default.md"
|
|
402
|
+
|
|
403
|
+
if user_default_path.exists() and user_default_path.resolve() != resolved_path:
|
|
404
|
+
if user_default_path.resolve() not in inheritance_chain:
|
|
405
|
+
try:
|
|
406
|
+
default_agent = load_extended_agent("default", agent_path, inheritance_chain.copy())
|
|
407
|
+
# User's default.md should opt out of builtin inheritance unless explicit
|
|
408
|
+
if default_agent.config.extends is None:
|
|
409
|
+
# Standalone user default
|
|
410
|
+
return default_agent.config, default_agent.content
|
|
411
|
+
else:
|
|
412
|
+
# User explicitly extended something, resolve it
|
|
413
|
+
default_agent = resolve_agent_inheritance(
|
|
414
|
+
default_agent, user_default_path, inheritance_chain.copy()
|
|
415
|
+
)
|
|
416
|
+
return default_agent.config, default_agent.content
|
|
417
|
+
except ValueError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# Fall back to config or builtin
|
|
421
|
+
default_base_name = _get_default_base_agent_name()
|
|
422
|
+
if not default_base_name:
|
|
423
|
+
return None, None
|
|
424
|
+
|
|
425
|
+
# Try to find the configured default agent (including built-ins via find_agent_file)
|
|
426
|
+
default_path = find_agent_file(default_base_name, agent_path.parent)
|
|
427
|
+
if default_path and default_path.resolve() != resolved_path:
|
|
428
|
+
if default_path.resolve() not in inheritance_chain:
|
|
429
|
+
try:
|
|
430
|
+
default_agent = load_extended_agent(default_base_name, agent_path, inheritance_chain.copy())
|
|
431
|
+
default_agent = resolve_agent_inheritance(default_agent, default_path, inheritance_chain.copy())
|
|
432
|
+
return default_agent.config, default_agent.content
|
|
433
|
+
except ValueError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
return None, None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def load_explicit_parent_agent(extends_ref: str, agent_path: Path, inheritance_chain: Set[Path]) -> tuple[Any, str]:
|
|
440
|
+
"""Load explicitly extended parent agent.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
extends_ref: Reference to parent agent
|
|
444
|
+
agent_path: Path to current agent file
|
|
445
|
+
inheritance_chain: Set of agent paths already in chain
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Tuple of (parent_config, parent_content)
|
|
449
|
+
"""
|
|
450
|
+
parent_agent = load_extended_agent(extends_ref, agent_path, inheritance_chain.copy())
|
|
451
|
+
parent_path = parent_agent.file_path
|
|
452
|
+
|
|
453
|
+
# Recursively resolve parent's inheritance
|
|
454
|
+
parent_agent = resolve_agent_inheritance(parent_agent, parent_path, inheritance_chain.copy())
|
|
455
|
+
return parent_agent.config, parent_agent.content
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def merge_configs_and_content(configs: List, contents: List):
|
|
459
|
+
"""Merge a list of configs and contents.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
configs: List of AgentConfig objects (earlier = lower precedence)
|
|
463
|
+
contents: List of content strings
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Tuple of (merged_config, merged_content)
|
|
467
|
+
"""
|
|
468
|
+
if len(configs) == 1:
|
|
469
|
+
return configs[0], contents[0]
|
|
470
|
+
|
|
471
|
+
# Merge configs in order
|
|
472
|
+
merged_config = configs[0]
|
|
473
|
+
for config in configs[1:]:
|
|
474
|
+
merged_config = merge_agent_configs(merged_config, config)
|
|
475
|
+
|
|
476
|
+
# Merge contents
|
|
477
|
+
merged_content = "\n\n".join(contents)
|
|
478
|
+
|
|
479
|
+
return merged_config, merged_content
|