indent 0.1.26__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.
- exponent/__init__.py +34 -0
- exponent/cli.py +110 -0
- exponent/commands/cloud_commands.py +585 -0
- exponent/commands/common.py +411 -0
- exponent/commands/config_commands.py +334 -0
- exponent/commands/run_commands.py +222 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +146 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +61 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/mutations.py +160 -0
- exponent/core/graphql/queries.py +146 -0
- exponent/core/graphql/subscriptions.py +16 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +499 -0
- exponent/core/remote_execution/client.py +999 -0
- exponent/core/remote_execution/code_execution.py +77 -0
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/error_info.py +45 -0
- exponent/core/remote_execution/exceptions.py +10 -0
- exponent/core/remote_execution/file_write.py +35 -0
- exponent/core/remote_execution/files.py +330 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/http_fetch.py +94 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +226 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +26 -0
- exponent/core/remote_execution/terminal_session.py +375 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +595 -0
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +296 -0
- exponent/core/remote_execution/types.py +635 -0
- exponent/core/remote_execution/utils.py +477 -0
- exponent/core/types/__init__.py +0 -0
- exponent/core/types/command_data.py +206 -0
- exponent/core/types/event_types.py +89 -0
- exponent/core/types/generated/__init__.py +0 -0
- exponent/core/types/generated/strategy_info.py +213 -0
- exponent/migration-docs/login.md +112 -0
- exponent/py.typed +4 -0
- exponent/utils/__init__.py +0 -0
- exponent/utils/colors.py +92 -0
- exponent/utils/version.py +289 -0
- indent-0.1.26.dist-info/METADATA +38 -0
- indent-0.1.26.dist-info/RECORD +55 -0
- indent-0.1.26.dist-info/WHEEL +4 -0
- indent-0.1.26.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Generalized truncation framework for tool results."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from msgspec.structs import replace
|
|
7
|
+
|
|
8
|
+
from exponent.core.remote_execution.cli_rpc_types import (
|
|
9
|
+
BashToolResult,
|
|
10
|
+
ErrorToolResult,
|
|
11
|
+
GlobToolResult,
|
|
12
|
+
GrepToolResult,
|
|
13
|
+
ListToolResult,
|
|
14
|
+
ReadToolResult,
|
|
15
|
+
ToolResult,
|
|
16
|
+
WriteToolResult,
|
|
17
|
+
)
|
|
18
|
+
from exponent.core.remote_execution.utils import truncate_output
|
|
19
|
+
|
|
20
|
+
DEFAULT_CHARACTER_LIMIT = 50_000
|
|
21
|
+
DEFAULT_LIST_ITEM_LIMIT = 1000
|
|
22
|
+
DEFAULT_LIST_PREVIEW_ITEMS = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TruncationStrategy(ABC):
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StringFieldTruncation(TruncationStrategy):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
field_name: str,
|
|
39
|
+
character_limit: int = DEFAULT_CHARACTER_LIMIT,
|
|
40
|
+
):
|
|
41
|
+
self.field_name = field_name
|
|
42
|
+
self.character_limit = character_limit
|
|
43
|
+
|
|
44
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
45
|
+
if hasattr(result, self.field_name):
|
|
46
|
+
value = getattr(result, self.field_name)
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
return len(value) > self.character_limit
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
52
|
+
if not hasattr(result, self.field_name):
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
value = getattr(result, self.field_name)
|
|
56
|
+
if not isinstance(value, str):
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
truncated_value, was_truncated = truncate_output(value, self.character_limit)
|
|
60
|
+
|
|
61
|
+
updates: dict[str, Any] = {self.field_name: truncated_value}
|
|
62
|
+
if hasattr(result, "truncated") and was_truncated:
|
|
63
|
+
updates["truncated"] = True
|
|
64
|
+
|
|
65
|
+
return replace(result, **updates)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ListFieldTruncation(TruncationStrategy):
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
field_name: str,
|
|
72
|
+
item_limit: int = DEFAULT_LIST_ITEM_LIMIT,
|
|
73
|
+
preview_items: int = DEFAULT_LIST_PREVIEW_ITEMS,
|
|
74
|
+
):
|
|
75
|
+
self.field_name = field_name
|
|
76
|
+
self.item_limit = item_limit
|
|
77
|
+
self.preview_items = preview_items
|
|
78
|
+
|
|
79
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
80
|
+
if hasattr(result, self.field_name):
|
|
81
|
+
value = getattr(result, self.field_name)
|
|
82
|
+
if isinstance(value, list):
|
|
83
|
+
return len(value) > self.item_limit
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
87
|
+
if not hasattr(result, self.field_name):
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
value = getattr(result, self.field_name)
|
|
91
|
+
if not isinstance(value, list):
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
total_items = len(value)
|
|
95
|
+
if total_items <= self.item_limit:
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
truncated_count = max(0, total_items - 2 * self.preview_items)
|
|
99
|
+
truncated_list = (
|
|
100
|
+
value[: self.preview_items]
|
|
101
|
+
+ [f"... {truncated_count} items truncated ..."]
|
|
102
|
+
+ value[-self.preview_items :]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
updates: dict[str, Any] = {self.field_name: truncated_list}
|
|
106
|
+
if hasattr(result, "truncated"):
|
|
107
|
+
updates["truncated"] = True
|
|
108
|
+
|
|
109
|
+
return replace(result, **updates)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CompositeTruncation(TruncationStrategy):
|
|
113
|
+
def __init__(self, strategies: list[TruncationStrategy]):
|
|
114
|
+
self.strategies = strategies
|
|
115
|
+
|
|
116
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
117
|
+
return any(strategy.should_truncate(result) for strategy in self.strategies)
|
|
118
|
+
|
|
119
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
120
|
+
for strategy in self.strategies:
|
|
121
|
+
if strategy.should_truncate(result):
|
|
122
|
+
result = strategy.truncate(result)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TailTruncation(TruncationStrategy):
|
|
127
|
+
"""Truncation strategy that keeps the end of the output (tail) instead of the beginning."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
field_name: str,
|
|
132
|
+
character_limit: int = DEFAULT_CHARACTER_LIMIT,
|
|
133
|
+
):
|
|
134
|
+
self.field_name = field_name
|
|
135
|
+
self.character_limit = character_limit
|
|
136
|
+
|
|
137
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
138
|
+
if hasattr(result, self.field_name):
|
|
139
|
+
value = getattr(result, self.field_name)
|
|
140
|
+
if isinstance(value, str):
|
|
141
|
+
return len(value) > self.character_limit
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
145
|
+
if not hasattr(result, self.field_name):
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
value = getattr(result, self.field_name)
|
|
149
|
+
if not isinstance(value, str):
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
if len(value) <= self.character_limit:
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
# Keep the last character_limit characters
|
|
156
|
+
truncated_value = value[-self.character_limit :]
|
|
157
|
+
|
|
158
|
+
# Try to start at a newline if possible for cleaner output
|
|
159
|
+
newline_pos = truncated_value.find("\n")
|
|
160
|
+
if (
|
|
161
|
+
newline_pos != -1 and newline_pos < 1000
|
|
162
|
+
): # Only adjust if newline is reasonably close to start
|
|
163
|
+
truncated_value = truncated_value[newline_pos + 1 :]
|
|
164
|
+
|
|
165
|
+
# Add truncation indicator at the beginning
|
|
166
|
+
truncation_msg = f"... (output truncated, showing last {len(truncated_value)} characters) ...\n"
|
|
167
|
+
truncated_value = truncation_msg + truncated_value
|
|
168
|
+
|
|
169
|
+
updates: dict[str, Any] = {self.field_name: truncated_value}
|
|
170
|
+
if hasattr(result, "truncated"):
|
|
171
|
+
updates["truncated"] = True
|
|
172
|
+
|
|
173
|
+
return replace(result, **updates)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class NoOpTruncation(TruncationStrategy):
|
|
177
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class StringListTruncation(TruncationStrategy):
|
|
185
|
+
"""Truncation for lists of strings that limits both number of items and individual string length."""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
field_name: str,
|
|
190
|
+
max_items: int = DEFAULT_LIST_ITEM_LIMIT,
|
|
191
|
+
preview_items: int = DEFAULT_LIST_PREVIEW_ITEMS,
|
|
192
|
+
max_item_length: int = 1000,
|
|
193
|
+
):
|
|
194
|
+
self.field_name = field_name
|
|
195
|
+
self.max_items = max_items
|
|
196
|
+
self.preview_items = preview_items
|
|
197
|
+
self.max_item_length = max_item_length
|
|
198
|
+
|
|
199
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
200
|
+
if not hasattr(result, self.field_name):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
items = getattr(result, self.field_name)
|
|
204
|
+
if not isinstance(items, list):
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
# Check if we need to truncate number of items
|
|
208
|
+
if len(items) > self.max_items:
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
# Check if any individual item is too long
|
|
212
|
+
for item in items:
|
|
213
|
+
if isinstance(item, str) and len(item) > self.max_item_length:
|
|
214
|
+
return True
|
|
215
|
+
# Handle dict items (e.g., with metadata like file path and line number)
|
|
216
|
+
elif isinstance(item, dict) and "content" in item:
|
|
217
|
+
if len(item["content"]) > self.max_item_length:
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def _truncate_item_content(
|
|
223
|
+
self, item: str | dict[str, Any]
|
|
224
|
+
) -> str | dict[str, Any]:
|
|
225
|
+
"""Truncate an individual item's content."""
|
|
226
|
+
if isinstance(item, str):
|
|
227
|
+
if len(item) <= self.max_item_length:
|
|
228
|
+
return item
|
|
229
|
+
# Truncate string item
|
|
230
|
+
truncated, _ = truncate_output(item, self.max_item_length)
|
|
231
|
+
return truncated
|
|
232
|
+
elif isinstance(item, dict) and "content" in item:
|
|
233
|
+
# Handle dict-style items (e.g., with metadata like file path and line number)
|
|
234
|
+
if len(item["content"]) <= self.max_item_length:
|
|
235
|
+
return item
|
|
236
|
+
truncated_content, _ = truncate_output(
|
|
237
|
+
item["content"], self.max_item_length
|
|
238
|
+
)
|
|
239
|
+
return {**item, "content": truncated_content}
|
|
240
|
+
else:
|
|
241
|
+
return item
|
|
242
|
+
|
|
243
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
244
|
+
if not hasattr(result, self.field_name):
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
items = getattr(result, self.field_name)
|
|
248
|
+
if not isinstance(items, list):
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
# First, truncate individual item contents
|
|
252
|
+
truncated_items = [self._truncate_item_content(item) for item in items]
|
|
253
|
+
|
|
254
|
+
# Then, limit the number of items if needed
|
|
255
|
+
total_items = len(truncated_items)
|
|
256
|
+
if total_items > self.max_items:
|
|
257
|
+
truncated_count = max(0, total_items - 2 * self.preview_items)
|
|
258
|
+
final_items = (
|
|
259
|
+
truncated_items[: self.preview_items]
|
|
260
|
+
+ [f"... {truncated_count} items truncated ..."]
|
|
261
|
+
+ truncated_items[-self.preview_items :]
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
final_items = truncated_items
|
|
265
|
+
|
|
266
|
+
updates: dict[str, Any] = {self.field_name: final_items}
|
|
267
|
+
if hasattr(result, "truncated"):
|
|
268
|
+
updates["truncated"] = True
|
|
269
|
+
|
|
270
|
+
return replace(result, **updates)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
|
|
274
|
+
ReadToolResult: StringFieldTruncation("content"),
|
|
275
|
+
WriteToolResult: StringFieldTruncation("message"),
|
|
276
|
+
BashToolResult: TailTruncation("shell_output"),
|
|
277
|
+
GrepToolResult: StringListTruncation("matches"),
|
|
278
|
+
GlobToolResult: StringListTruncation("filenames", max_item_length=4096),
|
|
279
|
+
ListToolResult: StringListTruncation("files", max_item_length=4096),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
T = TypeVar("T", bound=ToolResult)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def truncate_tool_result(result: T) -> T:
|
|
287
|
+
if isinstance(result, ErrorToolResult):
|
|
288
|
+
return cast(T, result)
|
|
289
|
+
|
|
290
|
+
result_type = type(result)
|
|
291
|
+
if result_type in TRUNCATION_REGISTRY:
|
|
292
|
+
strategy = TRUNCATION_REGISTRY[result_type]
|
|
293
|
+
if strategy.should_truncate(result):
|
|
294
|
+
return cast(T, strategy.truncate(result))
|
|
295
|
+
|
|
296
|
+
return result
|