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
tsugite/docker_cli.py ADDED
@@ -0,0 +1,270 @@
1
+ """Docker CLI wrapper entry points.
2
+
3
+ This module provides console script entry points for tsugite-docker and
4
+ tsugite-docker-session. The implementation is kept simple to maintain
5
+ the principle of zero coupling with tsugite core.
6
+ """
7
+
8
+ import argparse
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from uuid import uuid4
13
+
14
+
15
+ def docker_main():
16
+ """Entry point for tsugite-docker wrapper."""
17
+ # Parse wrapper-specific flags
18
+ parser = argparse.ArgumentParser(add_help=False)
19
+ parser.add_argument("--network", default="host", help="Docker network mode")
20
+ parser.add_argument("--keep", action="store_true", help="Keep container running")
21
+ parser.add_argument("--container", help="Use existing container or create named container")
22
+ parser.add_argument("--image", default="tsugite/runtime", help="Docker image to use")
23
+
24
+ args, tsugite_args = parser.parse_known_args()
25
+
26
+ # If using existing container, exec into it
27
+ if args.container:
28
+ # Check if container exists
29
+ check = subprocess.run(
30
+ ["docker", "ps", "-a", "-q", "-f", f"name=^{args.container}$"],
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+
36
+ if check.stdout.strip():
37
+ # Container exists - exec into it (tsugite command available in PATH)
38
+ cmd = ["docker", "exec", "-it", "-w", "/workspace", args.container, "tsugite"] + tsugite_args
39
+ else:
40
+ # Container doesn't exist - create it with this name
41
+ cmd = _build_run_command(args, tsugite_args, container_name=args.container)
42
+ else:
43
+ # New container
44
+ container_name = f"tsugite-{uuid4().hex[:8]}" if args.keep else None
45
+ cmd = _build_run_command(args, tsugite_args, container_name)
46
+
47
+ # Execute
48
+ result = subprocess.run(cmd, check=False)
49
+ sys.exit(result.returncode)
50
+
51
+
52
+ def _build_run_command(args, tsugite_args, container_name=None):
53
+ """Build docker run command."""
54
+ import os
55
+
56
+ cmd = ["docker", "run", "-it"]
57
+
58
+ # Container lifecycle
59
+ if container_name:
60
+ cmd.extend(["--name", container_name])
61
+ else:
62
+ cmd.append("--rm") # Auto-remove
63
+
64
+ # Network
65
+ cmd.extend(["--network", args.network])
66
+
67
+ # Volume mounts
68
+ # 1. Workspace (read-only for security)
69
+ cmd.extend(["-v", f"{Path.cwd()}:/workspace:ro"])
70
+
71
+ # 2. Config directory (read-only) - for MCP configs, model aliases, etc
72
+ config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) / "tsugite"
73
+ if config_dir.exists():
74
+ cmd.extend(["-v", f"{config_dir}:/root/.config/tsugite:ro"])
75
+
76
+ # 3. Cache directory (read-write) - for attachment cache
77
+ cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) / "tsugite"
78
+ if cache_dir.exists():
79
+ cmd.extend(["-v", f"{cache_dir}:/root/.cache/tsugite"])
80
+
81
+ # 4. Forward API keys and important env vars
82
+ env_vars = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GITHUB_TOKEN"]
83
+ for var in env_vars:
84
+ if var in os.environ:
85
+ cmd.extend(["-e", var])
86
+
87
+ # Working directory
88
+ cmd.extend(["-w", "/workspace"])
89
+
90
+ # Image and command (ENTRYPOINT already has "tsugite")
91
+ cmd.append(args.image)
92
+ cmd.extend(tsugite_args)
93
+
94
+ return cmd
95
+
96
+
97
+ def session_main():
98
+ """Entry point for tsugite-docker-session wrapper."""
99
+ if len(sys.argv) < 2:
100
+ _print_session_usage()
101
+ sys.exit(1)
102
+
103
+ command = sys.argv[1]
104
+
105
+ if command == "start":
106
+ _start_session(sys.argv[2:])
107
+ elif command == "stop":
108
+ _stop_session(sys.argv[2:])
109
+ elif command == "list":
110
+ _list_sessions()
111
+ elif command == "exec":
112
+ _exec_session(sys.argv[2:])
113
+ else:
114
+ print(f"Unknown command: {command}")
115
+ _print_session_usage()
116
+ sys.exit(1)
117
+
118
+
119
+ def _start_session(args):
120
+ """Start a persistent container."""
121
+ import os
122
+
123
+ if not args:
124
+ print("Error: session name required")
125
+ print("Usage: tsugite-docker-session start NAME [--network NETWORK] [--image IMAGE]")
126
+ sys.exit(1)
127
+
128
+ name = args[0]
129
+ network = "host" # Default to host for consistency
130
+ image = "tsugite/runtime"
131
+
132
+ # Parse optional flags
133
+ i = 1
134
+ while i < len(args):
135
+ if args[i] == "--network" and i + 1 < len(args):
136
+ network = args[i + 1]
137
+ i += 2
138
+ elif args[i] == "--image" and i + 1 < len(args):
139
+ image = args[i + 1]
140
+ i += 2
141
+ else:
142
+ i += 1
143
+
144
+ cmd = [
145
+ "docker",
146
+ "run",
147
+ "-d",
148
+ "--name",
149
+ name,
150
+ "--network",
151
+ network,
152
+ ]
153
+
154
+ # Volume mounts (same as run command)
155
+ cmd.extend(["-v", f"{Path.cwd()}:/workspace"])
156
+
157
+ # Config directory
158
+ config_dir = Path(os.getenv("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "tsugite"
159
+ if config_dir.exists():
160
+ cmd.extend(["-v", f"{config_dir}:/root/.config/tsugite:ro"])
161
+
162
+ # Cache directory
163
+ cache_dir = Path(os.getenv("XDG_CACHE_HOME", str(Path.home() / ".cache"))) / "tsugite"
164
+ if cache_dir.exists():
165
+ cmd.extend(["-v", f"{cache_dir}:/root/.cache/tsugite"])
166
+
167
+ # Forward API keys
168
+ env_vars = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GITHUB_TOKEN"]
169
+ for var in env_vars:
170
+ if var in os.environ:
171
+ cmd.extend(["-e", var])
172
+
173
+ cmd.extend(["-w", "/workspace", image, "tail", "-f", "/dev/null"])
174
+
175
+ result = subprocess.run(cmd, check=False)
176
+ if result.returncode == 0:
177
+ print(f"✓ Session '{name}' started")
178
+ print(f' Use: tsugite-docker --container {name} run agent.md "task"')
179
+ sys.exit(result.returncode)
180
+
181
+
182
+ def _stop_session(args):
183
+ """Stop a container."""
184
+ if not args:
185
+ print("Error: session name required")
186
+ print("Usage: tsugite-docker-session stop NAME [--remove]")
187
+ sys.exit(1)
188
+
189
+ name = args[0]
190
+ remove = "--remove" in args
191
+
192
+ # Stop container
193
+ result = subprocess.run(["docker", "stop", name], check=False)
194
+ if result.returncode != 0:
195
+ sys.exit(result.returncode)
196
+
197
+ # Remove if requested
198
+ if remove:
199
+ result = subprocess.run(["docker", "rm", name], check=False)
200
+ if result.returncode == 0:
201
+ print(f"✓ Session '{name}' stopped and removed")
202
+ else:
203
+ print(f"✓ Session '{name}' stopped")
204
+
205
+ sys.exit(result.returncode)
206
+
207
+
208
+ def _list_sessions():
209
+ """List all tsugite containers."""
210
+ subprocess.run(
211
+ [
212
+ "docker",
213
+ "ps",
214
+ "-a",
215
+ "--filter",
216
+ "name=tsugite-",
217
+ "--format",
218
+ "table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}",
219
+ ],
220
+ check=False,
221
+ )
222
+
223
+
224
+ def _exec_session(args):
225
+ """Execute command in container."""
226
+ if len(args) < 2:
227
+ print("Error: session name and command required")
228
+ print("Usage: tsugite-docker-session exec NAME COMMAND [ARGS...]")
229
+ sys.exit(1)
230
+
231
+ name = args[0]
232
+ command = args[1:]
233
+
234
+ # Check if running, start if needed
235
+ check = subprocess.run(["docker", "ps", "-q", "-f", f"name=^{name}$"], capture_output=True, text=True, check=False)
236
+
237
+ if not check.stdout.strip():
238
+ print(f"Starting stopped session '{name}'...")
239
+ subprocess.run(["docker", "start", name], check=False)
240
+
241
+ # Execute
242
+ result = subprocess.run(["docker", "exec", "-it", name] + command, check=False)
243
+ sys.exit(result.returncode)
244
+
245
+
246
+ def _print_session_usage():
247
+ """Print session management usage."""
248
+ print(
249
+ """Usage: tsugite-docker-session COMMAND [ARGS...]
250
+
251
+ Commands:
252
+ start NAME [--network NETWORK] [--image IMAGE]
253
+ Start a new persistent session
254
+
255
+ stop NAME [--remove]
256
+ Stop a session (optionally remove it)
257
+
258
+ list
259
+ List all tsugite sessions
260
+
261
+ exec NAME COMMAND [ARGS...]
262
+ Execute command in session
263
+
264
+ Examples:
265
+ tsugite-docker-session start my-work
266
+ tsugite-docker --container my-work run agent.md "task"
267
+ tsugite-docker-session exec my-work bash
268
+ tsugite-docker-session stop my-work
269
+ """
270
+ )
@@ -0,0 +1,55 @@
1
+ """Event system for UI and API communication."""
2
+
3
+ from .base import BaseEvent, EventType
4
+ from .bus import EventBus
5
+ from .events import (
6
+ CodeExecutionEvent,
7
+ CostSummaryEvent,
8
+ DebugMessageEvent,
9
+ ErrorEvent,
10
+ ExecutionLogsEvent,
11
+ ExecutionResultEvent,
12
+ FinalAnswerEvent,
13
+ InfoEvent,
14
+ LLMMessageEvent,
15
+ ObservationEvent,
16
+ ReasoningContentEvent,
17
+ ReasoningTokensEvent,
18
+ StepProgressEvent,
19
+ StepStartEvent,
20
+ StreamChunkEvent,
21
+ StreamCompleteEvent,
22
+ TaskStartEvent,
23
+ ToolCallEvent,
24
+ WarningEvent,
25
+ )
26
+
27
+ __all__ = [
28
+ # Base
29
+ "BaseEvent",
30
+ "EventType",
31
+ "EventBus",
32
+ # Execution
33
+ "TaskStartEvent",
34
+ "StepStartEvent",
35
+ "CodeExecutionEvent",
36
+ "ToolCallEvent",
37
+ "ObservationEvent",
38
+ "FinalAnswerEvent",
39
+ # LLM
40
+ "LLMMessageEvent",
41
+ "ExecutionResultEvent",
42
+ "ExecutionLogsEvent",
43
+ "ReasoningContentEvent",
44
+ "ReasoningTokensEvent",
45
+ # Meta
46
+ "InfoEvent",
47
+ "ErrorEvent",
48
+ "CostSummaryEvent",
49
+ "StreamChunkEvent",
50
+ "StreamCompleteEvent",
51
+ # Progress
52
+ "DebugMessageEvent",
53
+ "WarningEvent",
54
+ "StepProgressEvent",
55
+ ]
tsugite/events/base.py ADDED
@@ -0,0 +1,46 @@
1
+ """Base event model and event type enum."""
2
+
3
+ from datetime import datetime, timezone
4
+ from enum import IntEnum
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class EventType(IntEnum):
10
+ """Event type enumeration."""
11
+
12
+ # Core execution events
13
+ TASK_START = 1
14
+ STEP_START = 2
15
+ CODE_EXECUTION = 3
16
+ TOOL_CALL = 4
17
+ OBSERVATION = 5
18
+ ERROR = 8
19
+ FINAL_ANSWER = 9
20
+
21
+ # LLM events
22
+ LLM_MESSAGE = 10
23
+ EXECUTION_RESULT = 11
24
+ EXECUTION_LOGS = 12
25
+ REASONING_CONTENT = 13
26
+ REASONING_TOKENS = 14
27
+
28
+ # Meta events
29
+ COST_SUMMARY = 15
30
+ STREAM_CHUNK = 16
31
+ STREAM_COMPLETE = 17
32
+ INFO = 20
33
+
34
+ # New progress events
35
+ DEBUG_MESSAGE = 21
36
+ WARNING = 22
37
+ STEP_PROGRESS = 23
38
+
39
+
40
+ class BaseEvent(BaseModel):
41
+ """Base class for all UI events."""
42
+
43
+ model_config = {"frozen": True, "use_enum_values": False}
44
+
45
+ event_type: EventType = Field(frozen=True)
46
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
tsugite/events/bus.py ADDED
@@ -0,0 +1,62 @@
1
+ """EventBus for broadcasting events to multiple handlers."""
2
+
3
+ import sys
4
+ from typing import Callable, List
5
+
6
+ from .base import BaseEvent
7
+
8
+
9
+ class EventBus:
10
+ """Broadcast events to multiple handlers with error isolation."""
11
+
12
+ def __init__(self):
13
+ self._handlers: List[Callable[[BaseEvent], None]] = []
14
+
15
+ def subscribe(self, handler: Callable[[BaseEvent], None]) -> None:
16
+ """Subscribe a handler to receive events.
17
+
18
+ Args:
19
+ handler: Callable that accepts a BaseEvent
20
+ """
21
+ if handler not in self._handlers:
22
+ self._handlers.append(handler)
23
+
24
+ def unsubscribe(self, handler: Callable[[BaseEvent], None]) -> None:
25
+ """Unsubscribe a handler.
26
+
27
+ Args:
28
+ handler: Handler to remove
29
+ """
30
+ if handler in self._handlers:
31
+ self._handlers.remove(handler)
32
+
33
+ def emit(self, event: BaseEvent) -> None:
34
+ """Emit event to all subscribers with error isolation.
35
+
36
+ Args:
37
+ event: Event to broadcast
38
+ """
39
+ for handler in self._handlers:
40
+ try:
41
+ handler(event)
42
+ except Exception as e:
43
+ import traceback
44
+
45
+ print(f"Handler error: {e}", file=sys.stderr)
46
+ traceback.print_exc(file=sys.stderr)
47
+
48
+ def has_handlers(self) -> bool:
49
+ """Check if any handlers are subscribed.
50
+
51
+ Returns:
52
+ True if handlers exist
53
+ """
54
+ return len(self._handlers) > 0
55
+
56
+ def handler_count(self) -> int:
57
+ """Get number of subscribed handlers.
58
+
59
+ Returns:
60
+ Handler count
61
+ """
62
+ return len(self._handlers)
@@ -0,0 +1,224 @@
1
+ """All event classes consolidated in one module.
2
+
3
+ Error Handling Patterns:
4
+ ------------------------
5
+ 1. Tool Results (ObservationEvent):
6
+ - Success: ObservationEvent(success=True, observation="result", tool="tool_name")
7
+ - Failure: ObservationEvent(success=False, error="error msg", tool="tool_name")
8
+
9
+ 2. Code Execution (ExecutionResultEvent):
10
+ - Success: ExecutionResultEvent(success=True, logs=[...], output="result")
11
+ - Failure: ExecutionResultEvent(success=False, error="error msg")
12
+
13
+ 3. General/Fatal Errors (ErrorEvent):
14
+ - ErrorEvent(error="error msg", error_type="Error Type", step=N)
15
+ - Used for: Format errors, max turns exceeded, critical failures
16
+ """
17
+
18
+ from typing import Any, Dict, Optional
19
+
20
+ from pydantic import Field
21
+
22
+ from .base import BaseEvent, EventType
23
+
24
+ # ============================================================================
25
+ # Execution Events
26
+ # ============================================================================
27
+
28
+
29
+ class TaskStartEvent(BaseEvent):
30
+ """Agent execution starts."""
31
+
32
+ event_type: EventType = Field(default=EventType.TASK_START, frozen=True)
33
+ task: str
34
+ model: str
35
+
36
+
37
+ class StepStartEvent(BaseEvent):
38
+ """New reasoning turn."""
39
+
40
+ event_type: EventType = Field(default=EventType.STEP_START, frozen=True)
41
+ step: int = Field(ge=1)
42
+ max_turns: Optional[int] = Field(default=None, ge=1)
43
+
44
+
45
+ class CodeExecutionEvent(BaseEvent):
46
+ """Code being executed."""
47
+
48
+ event_type: EventType = Field(default=EventType.CODE_EXECUTION, frozen=True)
49
+ code: str
50
+ language: str = "python"
51
+
52
+
53
+ class ToolCallEvent(BaseEvent):
54
+ """Tool invocation."""
55
+
56
+ event_type: EventType = Field(default=EventType.TOOL_CALL, frozen=True)
57
+ tool: str
58
+ args: Dict[str, Any] = Field(default_factory=dict)
59
+
60
+
61
+ class ObservationEvent(BaseEvent):
62
+ """Observation from tool execution or code execution.
63
+
64
+ Usage:
65
+ - Tool success: ObservationEvent(success=True, observation="result", tool="tool_name")
66
+ - Tool failure: ObservationEvent(success=False, error="error", tool="tool_name")
67
+ - Code execution: ObservationEvent(observation="output", tool=None)
68
+
69
+ Note: For code execution, tool is None and ExecutionResultEvent may be emitted
70
+ separately with structured logs/output.
71
+ """
72
+
73
+ event_type: EventType = Field(default=EventType.OBSERVATION, frozen=True)
74
+ observation: str = ""
75
+ tool: Optional[str] = None
76
+ success: bool = True
77
+ error: Optional[str] = None
78
+
79
+
80
+ class FinalAnswerEvent(BaseEvent):
81
+ """Agent completed."""
82
+
83
+ event_type: EventType = Field(default=EventType.FINAL_ANSWER, frozen=True)
84
+ answer: str
85
+ turns: Optional[int] = Field(default=None, ge=1)
86
+ tokens: Optional[int] = Field(default=None, ge=0)
87
+ cost: Optional[float] = Field(default=None, ge=0)
88
+
89
+
90
+ # ============================================================================
91
+ # LLM Events
92
+ # ============================================================================
93
+
94
+
95
+ class LLMMessageEvent(BaseEvent):
96
+ """Reasoning/thought process."""
97
+
98
+ event_type: EventType = Field(default=EventType.LLM_MESSAGE, frozen=True)
99
+ content: str
100
+ title: Optional[str] = None
101
+ step: Optional[int] = Field(default=None, ge=1)
102
+
103
+
104
+ class ExecutionResultEvent(BaseEvent):
105
+ """Code execution result with structured logs and output."""
106
+
107
+ event_type: EventType = Field(default=EventType.EXECUTION_RESULT, frozen=True)
108
+ logs: list[str] = Field(default_factory=list)
109
+ output: Optional[str] = None
110
+ success: bool = True
111
+ error: Optional[str] = None
112
+
113
+
114
+ class ExecutionLogsEvent(BaseEvent):
115
+ """Execution logs."""
116
+
117
+ event_type: EventType = Field(default=EventType.EXECUTION_LOGS, frozen=True)
118
+ logs: str
119
+
120
+
121
+ class ReasoningContentEvent(BaseEvent):
122
+ """Model reasoning (Claude, Deepseek)."""
123
+
124
+ event_type: EventType = Field(default=EventType.REASONING_CONTENT, frozen=True)
125
+ content: str
126
+ step: Optional[int] = Field(default=None, ge=1)
127
+
128
+
129
+ class ReasoningTokensEvent(BaseEvent):
130
+ """Reasoning token count (o1, o3)."""
131
+
132
+ event_type: EventType = Field(default=EventType.REASONING_TOKENS, frozen=True)
133
+ tokens: int = Field(ge=0)
134
+ step: Optional[int] = Field(default=None, ge=1)
135
+
136
+
137
+ # ============================================================================
138
+ # Meta Events
139
+ # ============================================================================
140
+
141
+
142
+ class InfoEvent(BaseEvent):
143
+ """Informational message."""
144
+
145
+ event_type: EventType = Field(default=EventType.INFO, frozen=True)
146
+ message: str
147
+
148
+
149
+ class ErrorEvent(BaseEvent):
150
+ """Execution error."""
151
+
152
+ event_type: EventType = Field(default=EventType.ERROR, frozen=True)
153
+ error: str
154
+ error_type: Optional[str] = None
155
+ step: Optional[int] = Field(default=None, ge=1)
156
+ traceback: Optional[str] = None
157
+
158
+
159
+ class CostSummaryEvent(BaseEvent):
160
+ """Token/cost metrics with prompt caching support.
161
+
162
+ Prompt caching fields (supported by OpenAI, Anthropic, AWS Bedrock, Deepseek):
163
+ - cached_tokens: Total cached tokens read (unified across providers)
164
+ - cache_creation_input_tokens: Tokens used to create cache (Anthropic-specific)
165
+ - cache_read_input_tokens: Tokens read from cache (Anthropic-specific)
166
+
167
+ For Anthropic, both creation and read tokens are also included in cached_tokens.
168
+ """
169
+
170
+ event_type: EventType = Field(default=EventType.COST_SUMMARY, frozen=True)
171
+ tokens: Optional[int] = Field(default=None, ge=0)
172
+ cost: Optional[float] = Field(default=None, ge=0)
173
+ model: Optional[str] = None
174
+ duration_seconds: Optional[float] = Field(default=None, ge=0)
175
+
176
+ # Prompt caching fields
177
+ cached_tokens: Optional[int] = Field(default=None, ge=0)
178
+ cache_creation_input_tokens: Optional[int] = Field(default=None, ge=0)
179
+ cache_read_input_tokens: Optional[int] = Field(default=None, ge=0)
180
+
181
+
182
+ class StreamChunkEvent(BaseEvent):
183
+ """Streaming response chunk."""
184
+
185
+ event_type: EventType = Field(default=EventType.STREAM_CHUNK, frozen=True)
186
+ chunk: str
187
+
188
+
189
+ class StreamCompleteEvent(BaseEvent):
190
+ """Streaming finished."""
191
+
192
+ event_type: EventType = Field(default=EventType.STREAM_COMPLETE, frozen=True)
193
+ complete: bool = True
194
+
195
+
196
+ # ============================================================================
197
+ # Progress Events
198
+ # ============================================================================
199
+
200
+
201
+ class DebugMessageEvent(BaseEvent):
202
+ """Debug output."""
203
+
204
+ event_type: EventType = Field(default=EventType.DEBUG_MESSAGE, frozen=True)
205
+ message: str
206
+ context: Optional[Dict[str, Any]] = None
207
+
208
+
209
+ class WarningEvent(BaseEvent):
210
+ """Warning message."""
211
+
212
+ event_type: EventType = Field(default=EventType.WARNING, frozen=True)
213
+ message: str
214
+ category: Optional[str] = None
215
+
216
+
217
+ class StepProgressEvent(BaseEvent):
218
+ """Progress update."""
219
+
220
+ event_type: EventType = Field(default=EventType.STEP_PROGRESS, frozen=True)
221
+ message: str
222
+ step: Optional[int] = Field(default=None, ge=1)
223
+ total: Optional[int] = Field(default=None, ge=1)
224
+ percentage: Optional[float] = Field(default=None, ge=0, le=100)
tsugite/exceptions.py ADDED
@@ -0,0 +1,40 @@
1
+ """Custom exception classes for Tsugite."""
2
+
3
+ from typing import Any, List, Optional
4
+
5
+
6
+ class AgentExecutionError(RuntimeError):
7
+ """Exception raised when agent execution fails.
8
+
9
+ Includes execution details for debugging and analysis.
10
+
11
+ Attributes:
12
+ message: Error message
13
+ execution_steps: List of step results from agent execution
14
+ token_usage: Token usage count (if available)
15
+ cost: Cost of execution (if available)
16
+ step_count: Number of steps taken
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ message: str,
22
+ execution_steps: Optional[List[Any]] = None,
23
+ token_usage: Optional[int] = None,
24
+ cost: Optional[float] = None,
25
+ step_count: int = 0,
26
+ ):
27
+ """Initialize AgentExecutionError.
28
+
29
+ Args:
30
+ message: Error message
31
+ execution_steps: List of step results from execution
32
+ token_usage: Token usage count
33
+ cost: Cost of execution
34
+ step_count: Number of steps taken
35
+ """
36
+ super().__init__(message)
37
+ self.execution_steps = execution_steps or []
38
+ self.token_usage = token_usage
39
+ self.cost = cost
40
+ self.step_count = step_count