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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Agent preparation pipeline - unified logic for render and execution."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from tsugite.core.tools import Tool
|
|
8
|
+
from tsugite.md_agents import Agent, AgentConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PreparedAgent:
|
|
13
|
+
"""Fully prepared agent ready for execution or display.
|
|
14
|
+
|
|
15
|
+
This dataclass contains everything needed to either:
|
|
16
|
+
1. Display what will be sent to the LLM (render command)
|
|
17
|
+
2. Execute the agent (run command)
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
agent: Parsed agent object with content and config
|
|
21
|
+
agent_config: Agent configuration (model, tools, etc.)
|
|
22
|
+
system_message: Complete system message sent to LLM
|
|
23
|
+
user_message: Complete user message sent to LLM
|
|
24
|
+
rendered_prompt: Rendered template (before building system message)
|
|
25
|
+
tools: List of Tool objects ready for agent execution
|
|
26
|
+
context: Full template rendering context
|
|
27
|
+
combined_instructions: Combined default + agent instructions
|
|
28
|
+
prefetch_results: Results from prefetch tool execution
|
|
29
|
+
attachments: List of (name, content) tuples for prompt caching
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
agent: Agent
|
|
33
|
+
agent_config: AgentConfig
|
|
34
|
+
system_message: str
|
|
35
|
+
user_message: str
|
|
36
|
+
rendered_prompt: str
|
|
37
|
+
tools: List[Tool]
|
|
38
|
+
context: Dict[str, Any]
|
|
39
|
+
combined_instructions: str
|
|
40
|
+
prefetch_results: Dict[str, Any]
|
|
41
|
+
attachments: List[tuple[str, str]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentPreparer:
|
|
45
|
+
"""Prepares agents for execution or rendering.
|
|
46
|
+
|
|
47
|
+
This class consolidates all agent preparation logic that was previously
|
|
48
|
+
duplicated across render command, run_agent, and _execute_agent_with_prompt.
|
|
49
|
+
|
|
50
|
+
The preparation pipeline:
|
|
51
|
+
1. Parse agent file (with inheritance resolution)
|
|
52
|
+
2. Execute prefetch tools
|
|
53
|
+
3. Execute tool directives (optional)
|
|
54
|
+
4. Build template context
|
|
55
|
+
5. Render template
|
|
56
|
+
6. Build instructions
|
|
57
|
+
7. Expand and create tools
|
|
58
|
+
8. Build system prompt
|
|
59
|
+
|
|
60
|
+
This ensures that render shows EXACTLY what run executes.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def _extract_tool_directive_placeholders(self, content: str) -> Dict[str, str]:
|
|
64
|
+
"""Extract variable names from tool directives and return placeholders.
|
|
65
|
+
|
|
66
|
+
When rendering without executing directives, we still need variables to
|
|
67
|
+
be defined so the template doesn't fail. This extracts all assign="var"
|
|
68
|
+
names and creates placeholder values.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
content: Markdown content with tool directives
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict mapping variable names to placeholder values
|
|
75
|
+
"""
|
|
76
|
+
from tsugite.md_agents import extract_tool_directives
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
directives = extract_tool_directives(content)
|
|
80
|
+
except Exception:
|
|
81
|
+
# If extraction fails, return empty dict
|
|
82
|
+
return {}
|
|
83
|
+
|
|
84
|
+
placeholders = {}
|
|
85
|
+
for directive in directives:
|
|
86
|
+
if directive.assign_var:
|
|
87
|
+
# Create a descriptive placeholder showing what would be executed
|
|
88
|
+
placeholders[directive.assign_var] = (
|
|
89
|
+
f"[Tool directive: {directive.name}(...) - not executed in render mode]"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return placeholders
|
|
93
|
+
|
|
94
|
+
def prepare(
|
|
95
|
+
self,
|
|
96
|
+
agent: Agent,
|
|
97
|
+
prompt: str,
|
|
98
|
+
context: Optional[Dict[str, Any]] = None,
|
|
99
|
+
delegation_agents: Optional[List[tuple[str, Path]]] = None,
|
|
100
|
+
skip_tool_directives: bool = False,
|
|
101
|
+
task_summary: str = "## Current Tasks\nNo tasks yet.",
|
|
102
|
+
tasks: Optional[List[Dict[str, Any]]] = None,
|
|
103
|
+
attachments: Optional[List[tuple[str, str]]] = None,
|
|
104
|
+
) -> PreparedAgent:
|
|
105
|
+
"""Prepare agent with all context, tools, and instructions.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
agent: Parsed agent object
|
|
109
|
+
prompt: User prompt/task
|
|
110
|
+
context: Additional context variables
|
|
111
|
+
delegation_agents: List of (name, path) tuples for delegation
|
|
112
|
+
skip_tool_directives: Skip executing tool directives (for render)
|
|
113
|
+
task_summary: Current task summary (from task manager)
|
|
114
|
+
tasks: List of task dicts for template iteration (from task manager)
|
|
115
|
+
attachments: List of (name, content) tuples for prompt caching
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
PreparedAgent ready for execution or display
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
RuntimeError: If preparation fails
|
|
122
|
+
"""
|
|
123
|
+
from tsugite.agent_runner import (
|
|
124
|
+
_combine_instructions,
|
|
125
|
+
execute_prefetch,
|
|
126
|
+
execute_tool_directives,
|
|
127
|
+
get_default_instructions,
|
|
128
|
+
)
|
|
129
|
+
from tsugite.core.agent import build_system_prompt
|
|
130
|
+
from tsugite.core.tools import create_tool_from_tsugite
|
|
131
|
+
from tsugite.renderer import AgentRenderer
|
|
132
|
+
from tsugite.tools import expand_tool_specs
|
|
133
|
+
from tsugite.utils import is_interactive
|
|
134
|
+
|
|
135
|
+
if context is None:
|
|
136
|
+
context = {}
|
|
137
|
+
|
|
138
|
+
agent_config = agent.config
|
|
139
|
+
|
|
140
|
+
# Step 1: Execute prefetch tools
|
|
141
|
+
prefetch_context = {}
|
|
142
|
+
if agent_config.prefetch:
|
|
143
|
+
try:
|
|
144
|
+
from tsugite.agent_runner import execute_prefetch
|
|
145
|
+
|
|
146
|
+
prefetch_context = execute_prefetch(agent_config.prefetch)
|
|
147
|
+
except Exception:
|
|
148
|
+
# Silently continue if prefetch fails
|
|
149
|
+
prefetch_context = {}
|
|
150
|
+
|
|
151
|
+
# Step 2: Execute tool directives (unless skip_tool_directives=True for render)
|
|
152
|
+
if skip_tool_directives:
|
|
153
|
+
modified_content = agent.content
|
|
154
|
+
# Extract tool directive variable names and provide placeholders
|
|
155
|
+
tool_context = self._extract_tool_directive_placeholders(agent.content)
|
|
156
|
+
else:
|
|
157
|
+
modified_content, tool_context = execute_tool_directives(agent.content, prefetch_context)
|
|
158
|
+
|
|
159
|
+
# Step 3: Build template context
|
|
160
|
+
interactive_mode = is_interactive()
|
|
161
|
+
full_context = {
|
|
162
|
+
**context,
|
|
163
|
+
**prefetch_context,
|
|
164
|
+
**tool_context,
|
|
165
|
+
"user_prompt": prompt,
|
|
166
|
+
"task_summary": task_summary,
|
|
167
|
+
"tasks": tasks or [],
|
|
168
|
+
"is_interactive": interactive_mode,
|
|
169
|
+
"text_mode": agent_config.text_mode,
|
|
170
|
+
"tools": agent_config.tools,
|
|
171
|
+
# Subagent context
|
|
172
|
+
"is_subagent": context.get("is_subagent", False),
|
|
173
|
+
"parent_agent": context.get("parent_agent", None),
|
|
174
|
+
# Chat history (for chat agents)
|
|
175
|
+
"chat_history": context.get("chat_history", []),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Step 4: Render template
|
|
179
|
+
renderer = AgentRenderer()
|
|
180
|
+
try:
|
|
181
|
+
rendered_prompt = renderer.render(modified_content, full_context)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise RuntimeError(f"Template rendering failed: {e}") from e
|
|
184
|
+
|
|
185
|
+
# Step 5: Build instructions
|
|
186
|
+
base_instructions = get_default_instructions(text_mode=agent_config.text_mode)
|
|
187
|
+
agent_instructions = getattr(agent_config, "instructions", "")
|
|
188
|
+
|
|
189
|
+
# Render agent instructions as Jinja2 template (they may contain {% if text_mode %}, etc.)
|
|
190
|
+
if agent_instructions:
|
|
191
|
+
try:
|
|
192
|
+
agent_instructions = renderer.render(agent_instructions, full_context)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise RuntimeError(f"Failed to render agent instructions: {e}") from e
|
|
195
|
+
|
|
196
|
+
combined_instructions = _combine_instructions(base_instructions, agent_instructions)
|
|
197
|
+
|
|
198
|
+
# Step 6: Expand and create tools
|
|
199
|
+
try:
|
|
200
|
+
# Expand tool specifications (categories, globs, regular names)
|
|
201
|
+
expanded_tools = expand_tool_specs(agent_config.tools) if agent_config.tools else []
|
|
202
|
+
|
|
203
|
+
# Add task management tools
|
|
204
|
+
task_tools = ["task_add", "task_update", "task_complete", "task_list", "task_get"]
|
|
205
|
+
all_tool_names = expanded_tools + task_tools
|
|
206
|
+
|
|
207
|
+
if delegation_agents:
|
|
208
|
+
all_tool_names.append("spawn_agent")
|
|
209
|
+
|
|
210
|
+
# Filter out interactive tools in non-interactive mode
|
|
211
|
+
if not interactive_mode and "ask_user" in all_tool_names:
|
|
212
|
+
all_tool_names.remove("ask_user")
|
|
213
|
+
|
|
214
|
+
# Convert to Tool objects
|
|
215
|
+
tools = [create_tool_from_tsugite(name) for name in all_tool_names]
|
|
216
|
+
except Exception as e:
|
|
217
|
+
raise RuntimeError(f"Failed to create tools: {e}") from e
|
|
218
|
+
|
|
219
|
+
# Step 7: Build system message (what LLM actually sees)
|
|
220
|
+
system_message = build_system_prompt(tools, combined_instructions, agent_config.text_mode)
|
|
221
|
+
|
|
222
|
+
# User message is the rendered prompt
|
|
223
|
+
user_message = rendered_prompt
|
|
224
|
+
|
|
225
|
+
return PreparedAgent(
|
|
226
|
+
agent=agent,
|
|
227
|
+
agent_config=agent_config,
|
|
228
|
+
system_message=system_message,
|
|
229
|
+
user_message=user_message,
|
|
230
|
+
rendered_prompt=rendered_prompt,
|
|
231
|
+
tools=tools,
|
|
232
|
+
context=full_context,
|
|
233
|
+
combined_instructions=combined_instructions,
|
|
234
|
+
prefetch_results=prefetch_context,
|
|
235
|
+
attachments=attachments or [],
|
|
236
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Agent execution engine - public API."""
|
|
2
|
+
|
|
3
|
+
# Re-export public functions for backwards compatibility
|
|
4
|
+
from tsugite.agent_runner.helpers import ( # noqa: F401
|
|
5
|
+
clear_current_agent,
|
|
6
|
+
get_current_agent,
|
|
7
|
+
get_display_console,
|
|
8
|
+
get_ui_handler,
|
|
9
|
+
set_current_agent,
|
|
10
|
+
)
|
|
11
|
+
from tsugite.agent_runner.metrics import StepMetrics, display_step_metrics # noqa: F401
|
|
12
|
+
from tsugite.agent_runner.runner import ( # noqa: F401
|
|
13
|
+
_combine_instructions,
|
|
14
|
+
_execute_agent_with_prompt,
|
|
15
|
+
execute_prefetch,
|
|
16
|
+
execute_tool_directives,
|
|
17
|
+
get_default_instructions,
|
|
18
|
+
preview_multistep_agent,
|
|
19
|
+
run_agent,
|
|
20
|
+
run_agent_async,
|
|
21
|
+
run_multistep_agent,
|
|
22
|
+
run_multistep_agent_async,
|
|
23
|
+
)
|
|
24
|
+
from tsugite.agent_runner.validation import get_agent_info, validate_agent_file # noqa: F401
|
|
25
|
+
from tsugite.tools import call_tool # noqa: F401 - Re-export for test compatibility
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"run_agent",
|
|
29
|
+
"run_agent_async",
|
|
30
|
+
"run_multistep_agent",
|
|
31
|
+
"run_multistep_agent_async",
|
|
32
|
+
"preview_multistep_agent",
|
|
33
|
+
"execute_prefetch",
|
|
34
|
+
"execute_tool_directives",
|
|
35
|
+
"get_default_instructions",
|
|
36
|
+
"StepMetrics",
|
|
37
|
+
"display_step_metrics",
|
|
38
|
+
"validate_agent_file",
|
|
39
|
+
"get_agent_info",
|
|
40
|
+
"get_current_agent",
|
|
41
|
+
"set_current_agent",
|
|
42
|
+
"clear_current_agent",
|
|
43
|
+
"get_display_console",
|
|
44
|
+
"get_ui_handler",
|
|
45
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Shared helper functions for agent execution."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from tsugite.console import get_stderr_console
|
|
9
|
+
|
|
10
|
+
# Console for warnings and debug output (stderr)
|
|
11
|
+
_stderr_console = get_stderr_console()
|
|
12
|
+
|
|
13
|
+
# Thread-local storage for tracking currently executing agent
|
|
14
|
+
_current_agent_context = threading.local()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_current_agent(name: str) -> None:
|
|
18
|
+
"""Set the name of the currently executing agent in thread-local storage."""
|
|
19
|
+
_current_agent_context.name = name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_current_agent() -> Optional[str]:
|
|
23
|
+
"""Get the name of the currently executing agent from thread-local storage."""
|
|
24
|
+
return getattr(_current_agent_context, "name", None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clear_current_agent() -> None:
|
|
28
|
+
"""Clear the currently executing agent from thread-local storage."""
|
|
29
|
+
if hasattr(_current_agent_context, "name"):
|
|
30
|
+
delattr(_current_agent_context, "name")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_display_console(custom_logger: Optional[Any]) -> Console:
|
|
34
|
+
"""Get console for displaying output, with fallback to stderr.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
custom_logger: Custom logger instance (may be None)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Console instance to use for output
|
|
41
|
+
"""
|
|
42
|
+
if custom_logger and hasattr(custom_logger, "console"):
|
|
43
|
+
return custom_logger.console
|
|
44
|
+
return _stderr_console
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_ui_handler(custom_logger: Optional[Any]) -> Optional[Any]:
|
|
48
|
+
"""Safely get UI handler from custom logger.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
custom_logger: Custom logger instance (may be None)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
UI handler if available, None otherwise
|
|
55
|
+
"""
|
|
56
|
+
return custom_logger.ui_handler if custom_logger and hasattr(custom_logger, "ui_handler") else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_multistep_ui_context(custom_logger: Optional[Any], step_number: int, step_name: str, total_steps: int) -> None:
|
|
60
|
+
"""Set multistep context in UI handler if available.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
custom_logger: Custom logger instance (may be None)
|
|
64
|
+
step_number: Current step number
|
|
65
|
+
step_name: Name of current step
|
|
66
|
+
total_steps: Total number of steps
|
|
67
|
+
"""
|
|
68
|
+
ui_handler = get_ui_handler(custom_logger)
|
|
69
|
+
if ui_handler:
|
|
70
|
+
ui_handler.set_multistep_context(step_number, step_name, total_steps)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clear_multistep_ui_context(custom_logger: Optional[Any]) -> None:
|
|
74
|
+
"""Clear multistep context from UI handler if available.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
custom_logger: Custom logger instance (may be None)
|
|
78
|
+
"""
|
|
79
|
+
ui_handler = get_ui_handler(custom_logger)
|
|
80
|
+
if ui_handler:
|
|
81
|
+
ui_handler.clear_multistep_context()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_step_progress(
|
|
85
|
+
custom_logger: Optional[Any], step_header: str, message: str, debug: bool = False, style: str = "cyan"
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Print step progress message using event system.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
custom_logger: Custom logger instance (may be None)
|
|
91
|
+
step_header: Step header string
|
|
92
|
+
message: Message to display
|
|
93
|
+
debug: Whether debug mode is active (skips output if True)
|
|
94
|
+
style: Rich style string (e.g., "cyan", "green", "yellow")
|
|
95
|
+
"""
|
|
96
|
+
if debug:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Emit as StepProgressEvent through event bus
|
|
100
|
+
ui_handler = get_ui_handler(custom_logger)
|
|
101
|
+
if ui_handler:
|
|
102
|
+
from tsugite.events import EventBus, StepProgressEvent
|
|
103
|
+
|
|
104
|
+
event_bus = EventBus()
|
|
105
|
+
event_bus.subscribe(ui_handler.handle_event)
|
|
106
|
+
event_bus.emit(StepProgressEvent(message=f"{step_header} {message}", style=style))
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""History integration for agent runs."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _process_messages_field(turn, messages: list) -> None:
|
|
9
|
+
"""Process turn with messages field (full message history)."""
|
|
10
|
+
for msg in turn.messages:
|
|
11
|
+
if msg.get("role") != "system":
|
|
12
|
+
messages.append(msg)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _process_steps_field(turn, messages: list) -> None:
|
|
16
|
+
"""Process turn with steps field (execution steps)."""
|
|
17
|
+
messages.append({"role": "user", "content": turn.user})
|
|
18
|
+
for step in turn.steps:
|
|
19
|
+
thought = step.get("thought", "")
|
|
20
|
+
code = step.get("code", "")
|
|
21
|
+
output = step.get("output", "")
|
|
22
|
+
error = step.get("error")
|
|
23
|
+
|
|
24
|
+
messages.append({"role": "assistant", "content": f"Thought: {thought}\n\n```python\n{code}\n```"})
|
|
25
|
+
|
|
26
|
+
observation = f"Observation: {output}"
|
|
27
|
+
if error:
|
|
28
|
+
observation += f"\nError: {error}"
|
|
29
|
+
messages.append({"role": "user", "content": observation})
|
|
30
|
+
|
|
31
|
+
messages.append({"role": "assistant", "content": turn.assistant})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _process_simple_turn(turn, messages: list) -> None:
|
|
35
|
+
"""Process simple turn (user/assistant only)."""
|
|
36
|
+
messages.append({"role": "user", "content": turn.user})
|
|
37
|
+
messages.append({"role": "assistant", "content": turn.assistant})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_conversation_messages(conversation_id: str) -> list[dict]:
|
|
41
|
+
"""Load conversation history as message list for LLM.
|
|
42
|
+
|
|
43
|
+
Loads complete message history including tool calls and intermediate steps.
|
|
44
|
+
System messages are skipped as they will be reconstructed with current context.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
conversation_id: Conversation ID to load
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of message dicts including all tool calls and observations
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
FileNotFoundError: If conversation doesn't exist
|
|
54
|
+
RuntimeError: If load fails
|
|
55
|
+
"""
|
|
56
|
+
from tsugite.ui.chat_history import load_conversation_history
|
|
57
|
+
|
|
58
|
+
turns = load_conversation_history(conversation_id)
|
|
59
|
+
|
|
60
|
+
messages = []
|
|
61
|
+
for turn in turns:
|
|
62
|
+
if turn.messages:
|
|
63
|
+
_process_messages_field(turn, messages)
|
|
64
|
+
elif turn.steps:
|
|
65
|
+
_process_steps_field(turn, messages)
|
|
66
|
+
else:
|
|
67
|
+
_process_simple_turn(turn, messages)
|
|
68
|
+
|
|
69
|
+
return messages
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def apply_cache_control_to_messages(messages: list[dict]) -> list[dict]:
|
|
73
|
+
"""Apply cache control markers to all conversation messages.
|
|
74
|
+
|
|
75
|
+
Following industry best practices from Anthropic and OpenAI, we cache
|
|
76
|
+
all conversation history.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
messages: List of message dicts (user/assistant pairs)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of message dicts with cache_control added to all messages
|
|
83
|
+
"""
|
|
84
|
+
if not messages:
|
|
85
|
+
return messages
|
|
86
|
+
|
|
87
|
+
return [{**msg, "cache_control": {"type": "ephemeral"}} for msg in messages]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_and_apply_history(conversation_id: str) -> list[dict]:
|
|
91
|
+
"""Load conversation history and apply cache control markers.
|
|
92
|
+
|
|
93
|
+
Consolidates the common pattern of loading conversation messages
|
|
94
|
+
and applying cache control for optimal performance.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
conversation_id: Conversation ID to load
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of message dicts with cache control applied
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If conversation not found
|
|
104
|
+
RuntimeError: If loading fails
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
messages = load_conversation_messages(conversation_id)
|
|
108
|
+
if messages:
|
|
109
|
+
messages = apply_cache_control_to_messages(messages)
|
|
110
|
+
return messages
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
raise ValueError(f"Conversation not found: {conversation_id}")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise RuntimeError(f"Failed to load conversation history: {e}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def save_run_to_history(
|
|
118
|
+
agent_path: Path,
|
|
119
|
+
agent_name: str,
|
|
120
|
+
prompt: str,
|
|
121
|
+
result: str,
|
|
122
|
+
model: str,
|
|
123
|
+
token_count: Optional[int] = None,
|
|
124
|
+
cost: Optional[float] = None,
|
|
125
|
+
execution_steps: Optional[list] = None,
|
|
126
|
+
continue_conversation_id: Optional[str] = None,
|
|
127
|
+
system_prompt: Optional[str] = None,
|
|
128
|
+
attachments: Optional[list] = None,
|
|
129
|
+
) -> Optional[str]:
|
|
130
|
+
"""Save a single agent run to history.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
agent_path: Path to agent file
|
|
134
|
+
agent_name: Name of the agent
|
|
135
|
+
prompt: User prompt/task
|
|
136
|
+
result: Agent's final answer
|
|
137
|
+
model: Model used
|
|
138
|
+
token_count: Number of tokens used
|
|
139
|
+
cost: Cost of execution
|
|
140
|
+
execution_steps: List of execution steps (from agent memory)
|
|
141
|
+
continue_conversation_id: Optional conversation ID to continue (for multi-turn run mode)
|
|
142
|
+
system_prompt: System prompt sent to LLM
|
|
143
|
+
attachments: List of (name, content) tuples for attachments
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Conversation ID if saved, None if history disabled or failed
|
|
147
|
+
"""
|
|
148
|
+
# Don't save subagent runs to history
|
|
149
|
+
import os
|
|
150
|
+
|
|
151
|
+
if os.environ.get("TSUGITE_SUBAGENT_MODE") == "1":
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
from tsugite.config import load_config
|
|
156
|
+
from tsugite.ui.chat_history import save_chat_turn, start_conversation
|
|
157
|
+
|
|
158
|
+
config = load_config()
|
|
159
|
+
if not getattr(config, "history_enabled", True):
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
from tsugite.md_agents import parse_agent_file
|
|
164
|
+
|
|
165
|
+
agent = parse_agent_file(agent_path)
|
|
166
|
+
if getattr(agent.config, "disable_history", False):
|
|
167
|
+
return None
|
|
168
|
+
except Exception as e:
|
|
169
|
+
import sys
|
|
170
|
+
|
|
171
|
+
print(f"Warning: Could not check agent history settings: {e}", file=sys.stderr)
|
|
172
|
+
|
|
173
|
+
timestamp = datetime.now(timezone.utc)
|
|
174
|
+
|
|
175
|
+
if continue_conversation_id:
|
|
176
|
+
conv_id = continue_conversation_id
|
|
177
|
+
else:
|
|
178
|
+
conv_id = start_conversation(
|
|
179
|
+
agent_name=agent_name,
|
|
180
|
+
model=model,
|
|
181
|
+
timestamp=timestamp,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
messages = []
|
|
185
|
+
|
|
186
|
+
if system_prompt or attachments:
|
|
187
|
+
if attachments:
|
|
188
|
+
system_blocks = [{"type": "text", "text": system_prompt or ""}]
|
|
189
|
+
for name, content in attachments:
|
|
190
|
+
system_blocks.append(
|
|
191
|
+
{
|
|
192
|
+
"type": "text",
|
|
193
|
+
"text": f"<Attachment: {name}>\n{content}\n</Attachment: {name}>",
|
|
194
|
+
"cache_control": {"type": "ephemeral"},
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
messages.append({"role": "system", "content": system_blocks})
|
|
198
|
+
else:
|
|
199
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
200
|
+
|
|
201
|
+
messages.append({"role": "user", "content": prompt})
|
|
202
|
+
|
|
203
|
+
if execution_steps:
|
|
204
|
+
for step in execution_steps:
|
|
205
|
+
thought = getattr(step, "thought", "")
|
|
206
|
+
code = getattr(step, "code", "")
|
|
207
|
+
output = getattr(step, "output", "")
|
|
208
|
+
|
|
209
|
+
messages.append({"role": "assistant", "content": f"Thought: {thought}\n\n```python\n{code}\n```"})
|
|
210
|
+
messages.append({"role": "user", "content": f"Observation: {output}"})
|
|
211
|
+
|
|
212
|
+
messages.append({"role": "assistant", "content": result})
|
|
213
|
+
|
|
214
|
+
save_chat_turn(
|
|
215
|
+
conversation_id=conv_id,
|
|
216
|
+
user_message=prompt,
|
|
217
|
+
agent_response=result,
|
|
218
|
+
tool_calls=_extract_tool_calls(execution_steps) if execution_steps else [],
|
|
219
|
+
token_count=token_count,
|
|
220
|
+
cost=cost,
|
|
221
|
+
timestamp=timestamp,
|
|
222
|
+
execution_steps=execution_steps,
|
|
223
|
+
messages=messages,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return conv_id
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
import sys
|
|
230
|
+
|
|
231
|
+
print(f"Warning: Failed to save run to history: {e}", file=sys.stderr)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _extract_tool_calls(execution_steps: list) -> list[str]:
|
|
236
|
+
"""Extract list of tool names called during execution.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
execution_steps: List of execution step objects
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of unique tool names called
|
|
243
|
+
"""
|
|
244
|
+
tool_calls = set()
|
|
245
|
+
for step in execution_steps:
|
|
246
|
+
if hasattr(step, "tools_called") and step.tools_called:
|
|
247
|
+
tool_calls.update(step.tools_called)
|
|
248
|
+
return sorted(list(tool_calls))
|