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/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)
|
tsugite/events/events.py
ADDED
|
@@ -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
|