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.
Files changed (55) hide show
  1. exponent/__init__.py +34 -0
  2. exponent/cli.py +110 -0
  3. exponent/commands/cloud_commands.py +585 -0
  4. exponent/commands/common.py +411 -0
  5. exponent/commands/config_commands.py +334 -0
  6. exponent/commands/run_commands.py +222 -0
  7. exponent/commands/settings.py +56 -0
  8. exponent/commands/types.py +111 -0
  9. exponent/commands/upgrade.py +29 -0
  10. exponent/commands/utils.py +146 -0
  11. exponent/core/config.py +180 -0
  12. exponent/core/graphql/__init__.py +0 -0
  13. exponent/core/graphql/client.py +61 -0
  14. exponent/core/graphql/get_chats_query.py +47 -0
  15. exponent/core/graphql/mutations.py +160 -0
  16. exponent/core/graphql/queries.py +146 -0
  17. exponent/core/graphql/subscriptions.py +16 -0
  18. exponent/core/remote_execution/checkpoints.py +212 -0
  19. exponent/core/remote_execution/cli_rpc_types.py +499 -0
  20. exponent/core/remote_execution/client.py +999 -0
  21. exponent/core/remote_execution/code_execution.py +77 -0
  22. exponent/core/remote_execution/default_env.py +31 -0
  23. exponent/core/remote_execution/error_info.py +45 -0
  24. exponent/core/remote_execution/exceptions.py +10 -0
  25. exponent/core/remote_execution/file_write.py +35 -0
  26. exponent/core/remote_execution/files.py +330 -0
  27. exponent/core/remote_execution/git.py +268 -0
  28. exponent/core/remote_execution/http_fetch.py +94 -0
  29. exponent/core/remote_execution/languages/python_execution.py +239 -0
  30. exponent/core/remote_execution/languages/shell_streaming.py +226 -0
  31. exponent/core/remote_execution/languages/types.py +20 -0
  32. exponent/core/remote_execution/port_utils.py +73 -0
  33. exponent/core/remote_execution/session.py +128 -0
  34. exponent/core/remote_execution/system_context.py +26 -0
  35. exponent/core/remote_execution/terminal_session.py +375 -0
  36. exponent/core/remote_execution/terminal_types.py +29 -0
  37. exponent/core/remote_execution/tool_execution.py +595 -0
  38. exponent/core/remote_execution/tool_type_utils.py +39 -0
  39. exponent/core/remote_execution/truncation.py +296 -0
  40. exponent/core/remote_execution/types.py +635 -0
  41. exponent/core/remote_execution/utils.py +477 -0
  42. exponent/core/types/__init__.py +0 -0
  43. exponent/core/types/command_data.py +206 -0
  44. exponent/core/types/event_types.py +89 -0
  45. exponent/core/types/generated/__init__.py +0 -0
  46. exponent/core/types/generated/strategy_info.py +213 -0
  47. exponent/migration-docs/login.md +112 -0
  48. exponent/py.typed +4 -0
  49. exponent/utils/__init__.py +0 -0
  50. exponent/utils/colors.py +92 -0
  51. exponent/utils/version.py +289 -0
  52. indent-0.1.26.dist-info/METADATA +38 -0
  53. indent-0.1.26.dist-info/RECORD +55 -0
  54. indent-0.1.26.dist-info/WHEEL +4 -0
  55. 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