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.
Files changed (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. 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))