indent 0.1.9__py3-none-any.whl → 0.1.11__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.
Potentially problematic release.
This version of indent might be problematic. Click here for more details.
- exponent/__init__.py +16 -3
- exponent/commands/config_commands.py +1 -161
- exponent/commands/utils.py +3 -3
- exponent/commands/workflow_commands.py +1 -1
- exponent/core/config.py +0 -2
- exponent/core/graphql/subscriptions.py +0 -449
- exponent/core/remote_execution/cli_rpc_types.py +31 -1
- exponent/core/remote_execution/client.py +6 -7
- exponent/core/remote_execution/file_write.py +2 -2
- exponent/core/remote_execution/files.py +1 -102
- exponent/core/remote_execution/git.py +1 -1
- exponent/core/remote_execution/http_fetch.py +3 -4
- exponent/core/remote_execution/languages/python_execution.py +1 -1
- exponent/core/remote_execution/languages/shell_streaming.py +1 -1
- exponent/core/remote_execution/session.py +1 -1
- exponent/core/remote_execution/system_context.py +0 -3
- exponent/core/remote_execution/tool_execution.py +24 -4
- exponent/core/remote_execution/truncation.py +51 -47
- exponent/core/remote_execution/types.py +24 -72
- exponent/core/remote_execution/utils.py +23 -51
- exponent/core/types/event_types.py +2 -2
- exponent/utils/version.py +1 -1
- {indent-0.1.9.dist-info → indent-0.1.11.dist-info}/METADATA +1 -1
- {indent-0.1.9.dist-info → indent-0.1.11.dist-info}/RECORD +26 -27
- exponent/core/graphql/cloud_config_queries.py +0 -77
- {indent-0.1.9.dist-info → indent-0.1.11.dist-info}/WHEEL +0 -0
- {indent-0.1.9.dist-info → indent-0.1.11.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
1
3
|
import msgspec
|
|
2
4
|
import yaml
|
|
3
|
-
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from exponent_server.core.tools.edit_tool import (
|
|
8
|
+
EditToolInput as ServerSideEditToolInput,
|
|
9
|
+
)
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
class PartialToolResult(msgspec.Struct, tag_field="tool_name", omit_defaults=True):
|
|
@@ -46,11 +52,17 @@ class ReadToolInput(ToolInput, tag=READ_TOOL_NAME):
|
|
|
46
52
|
limit: int | None = None
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
class FileMetadata(msgspec.Struct):
|
|
56
|
+
modified_timestamp: float
|
|
57
|
+
file_mode: str
|
|
58
|
+
|
|
59
|
+
|
|
49
60
|
class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
|
|
50
61
|
content: str
|
|
51
62
|
num_lines: int
|
|
52
63
|
start_line: int
|
|
53
64
|
total_lines: int
|
|
65
|
+
metadata: FileMetadata | None = None
|
|
54
66
|
|
|
55
67
|
def to_text(self) -> str:
|
|
56
68
|
lines = self.content.splitlines()
|
|
@@ -99,6 +111,7 @@ class WriteToolInput(ToolInput, tag=WRITE_TOOL_NAME):
|
|
|
99
111
|
|
|
100
112
|
class WriteToolResult(ToolResult, tag=WRITE_TOOL_NAME):
|
|
101
113
|
message: str
|
|
114
|
+
metadata: FileMetadata | None = None
|
|
102
115
|
|
|
103
116
|
|
|
104
117
|
GREP_TOOL_NAME = "grep"
|
|
@@ -125,10 +138,27 @@ class EditToolInput(ToolInput, tag=EDIT_TOOL_NAME):
|
|
|
125
138
|
old_string: str
|
|
126
139
|
new_string: str
|
|
127
140
|
replace_all: bool = False
|
|
141
|
+
# The last retrieved modified timestamp. This is used to check if the file has been modified since the last read/write that we are aware of (in which case we should re-read the file before editing).
|
|
142
|
+
last_known_modified_timestamp: float | None = None
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_server_side_input(
|
|
146
|
+
cls,
|
|
147
|
+
server_side_input: "ServerSideEditToolInput",
|
|
148
|
+
last_known_modified_timestamp: float | None,
|
|
149
|
+
) -> "EditToolInput":
|
|
150
|
+
return cls(
|
|
151
|
+
file_path=server_side_input.file_path,
|
|
152
|
+
old_string=server_side_input.old_string,
|
|
153
|
+
new_string=server_side_input.new_string,
|
|
154
|
+
replace_all=server_side_input.replace_all,
|
|
155
|
+
last_known_modified_timestamp=last_known_modified_timestamp,
|
|
156
|
+
)
|
|
128
157
|
|
|
129
158
|
|
|
130
159
|
class EditToolResult(ToolResult, tag=EDIT_TOOL_NAME):
|
|
131
160
|
message: str
|
|
161
|
+
metadata: FileMetadata | None = None
|
|
132
162
|
|
|
133
163
|
|
|
134
164
|
BASH_TOOL_NAME = "bash"
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
from collections.abc import AsyncGenerator, Callable
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from typing import Any, TypeVar,
|
|
9
|
+
from typing import Any, TypeVar, cast
|
|
10
10
|
|
|
11
11
|
import msgspec
|
|
12
12
|
import websockets.client
|
|
@@ -29,15 +29,14 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
29
29
|
ErrorResponse,
|
|
30
30
|
GetAllFilesRequest,
|
|
31
31
|
GetAllFilesResponse,
|
|
32
|
-
HttpResponse,
|
|
33
32
|
HttpRequest,
|
|
33
|
+
SwitchCLIChatRequest,
|
|
34
|
+
SwitchCLIChatResponse,
|
|
34
35
|
TerminateRequest,
|
|
35
36
|
TerminateResponse,
|
|
36
37
|
ToolExecutionRequest,
|
|
37
38
|
ToolExecutionResponse,
|
|
38
39
|
ToolResultType,
|
|
39
|
-
SwitchCLIChatRequest,
|
|
40
|
-
SwitchCLIChatResponse,
|
|
41
40
|
)
|
|
42
41
|
from exponent.core.remote_execution.code_execution import (
|
|
43
42
|
execute_code_streaming,
|
|
@@ -87,7 +86,7 @@ class SwitchCLIChat:
|
|
|
87
86
|
new_chat_uuid: str
|
|
88
87
|
|
|
89
88
|
|
|
90
|
-
REMOTE_EXECUTION_CLIENT_EXIT_INFO =
|
|
89
|
+
REMOTE_EXECUTION_CLIENT_EXIT_INFO = WSDisconnected | SwitchCLIChat
|
|
91
90
|
|
|
92
91
|
|
|
93
92
|
class RemoteExecutionClient:
|
|
@@ -302,7 +301,7 @@ class RemoteExecutionClient:
|
|
|
302
301
|
async with results_lock:
|
|
303
302
|
logger.info(f"Putting response {response}")
|
|
304
303
|
await results.put(response)
|
|
305
|
-
except Exception as e:
|
|
304
|
+
except Exception as e:
|
|
306
305
|
logger.info(f"Error handling request {request}:\n\n{e}")
|
|
307
306
|
async with results_lock:
|
|
308
307
|
await results.put(
|
|
@@ -550,7 +549,7 @@ class RemoteExecutionClient:
|
|
|
550
549
|
)
|
|
551
550
|
tool_result = truncate_result(raw_result)
|
|
552
551
|
results.append(tool_result)
|
|
553
|
-
except Exception as e:
|
|
552
|
+
except Exception as e:
|
|
554
553
|
logger.error(f"Error executing tool {tool_input}: {e}")
|
|
555
554
|
from exponent.core.remote_execution.cli_rpc_types import (
|
|
556
555
|
ErrorToolResult,
|
|
@@ -77,7 +77,7 @@ def lint_file(file_path: str, working_directory: str) -> str:
|
|
|
77
77
|
|
|
78
78
|
# If the subprocess ran successfully, return a success message
|
|
79
79
|
return f"Lint results:\n\n{result.stdout}\n\n{result.stderr}"
|
|
80
|
-
except Exception as e:
|
|
80
|
+
except Exception as e:
|
|
81
81
|
# For any other errors, return a generic error message
|
|
82
82
|
return f"An error occurred while linting: {e!s}"
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ async def execute_full_file_rewrite(
|
|
|
103
103
|
|
|
104
104
|
return result
|
|
105
105
|
|
|
106
|
-
except Exception as e:
|
|
106
|
+
except Exception as e:
|
|
107
107
|
return f"An error occurred: {e!s}"
|
|
108
108
|
|
|
109
109
|
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from asyncio import
|
|
2
|
+
from asyncio import to_thread
|
|
3
3
|
from typing import Final, cast
|
|
4
4
|
|
|
5
5
|
from anyio import Path as AsyncPath
|
|
6
6
|
from python_ripgrep import PySortMode, PySortModeKind, files, search
|
|
7
|
-
from rapidfuzz import process
|
|
8
7
|
|
|
9
8
|
from exponent.core.remote_execution.cli_rpc_types import ErrorToolResult, GrepToolResult
|
|
10
9
|
from exponent.core.remote_execution.types import (
|
|
11
|
-
FileAttachment,
|
|
12
10
|
FilePath,
|
|
13
|
-
GetFileAttachmentRequest,
|
|
14
|
-
GetFileAttachmentResponse,
|
|
15
|
-
GetFileAttachmentsRequest,
|
|
16
|
-
GetFileAttachmentsResponse,
|
|
17
|
-
GetMatchingFilesRequest,
|
|
18
|
-
GetMatchingFilesResponse,
|
|
19
11
|
ListFilesRequest,
|
|
20
12
|
ListFilesResponse,
|
|
21
13
|
RemoteFile,
|
|
@@ -114,99 +106,6 @@ async def get_file_content(
|
|
|
114
106
|
return content, exists
|
|
115
107
|
|
|
116
108
|
|
|
117
|
-
async def get_file_attachments(
|
|
118
|
-
get_file_attachments_request: GetFileAttachmentsRequest,
|
|
119
|
-
client_working_directory: str,
|
|
120
|
-
) -> GetFileAttachmentsResponse:
|
|
121
|
-
"""Get the content of the files at the specified paths.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
get_file_attachments_request: An object containing the file paths.
|
|
125
|
-
client_working_directory: The working directory of the client.
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
A list of FileAttachment objects containing the content of the files.
|
|
129
|
-
"""
|
|
130
|
-
remote_files = get_file_attachments_request.files
|
|
131
|
-
attachments = await gather(
|
|
132
|
-
*[
|
|
133
|
-
get_file_content(
|
|
134
|
-
AsyncPath(client_working_directory) / remote_file.file_path
|
|
135
|
-
)
|
|
136
|
-
for remote_file in remote_files
|
|
137
|
-
]
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
files = [
|
|
141
|
-
FileAttachment(attachment_type="file", file=remote_file, content=content)
|
|
142
|
-
for remote_file, (content, _) in zip(remote_files, attachments)
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
return GetFileAttachmentsResponse(
|
|
146
|
-
correlation_id=get_file_attachments_request.correlation_id,
|
|
147
|
-
file_attachments=files,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
async def get_file_attachment(
|
|
152
|
-
get_file_attachment_request: GetFileAttachmentRequest, client_working_directory: str
|
|
153
|
-
) -> GetFileAttachmentResponse:
|
|
154
|
-
"""Get the content of the file at the specified path.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
get_file_attachment_request: An object containing the file path.
|
|
158
|
-
client_working_directory: The working directory of the client.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
A FileAttachment object containing the content of the file.
|
|
162
|
-
"""
|
|
163
|
-
file = get_file_attachment_request.file
|
|
164
|
-
absolute_path = await file.resolve(client_working_directory)
|
|
165
|
-
|
|
166
|
-
content, exists = await get_file_content(absolute_path)
|
|
167
|
-
|
|
168
|
-
return GetFileAttachmentResponse(
|
|
169
|
-
content=content,
|
|
170
|
-
exists=exists,
|
|
171
|
-
file=file,
|
|
172
|
-
correlation_id=get_file_attachment_request.correlation_id,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
async def get_matching_files(
|
|
177
|
-
search_term: GetMatchingFilesRequest,
|
|
178
|
-
file_cache: FileCache,
|
|
179
|
-
) -> GetMatchingFilesResponse:
|
|
180
|
-
"""Get the files that match the search term.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
search_term: The search term to match against the files.
|
|
184
|
-
file_cache: A cache of the files in the working directory.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
A list of RemoteFile objects that match the search term.
|
|
188
|
-
"""
|
|
189
|
-
# Use rapidfuzz to find the best matching files
|
|
190
|
-
matching_files = await to_thread(
|
|
191
|
-
process.extract,
|
|
192
|
-
search_term.search_term,
|
|
193
|
-
await file_cache.get_files(),
|
|
194
|
-
limit=MAX_MATCHING_FILES,
|
|
195
|
-
score_cutoff=0,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
directory = file_cache.working_directory
|
|
199
|
-
files: list[RemoteFile] = [
|
|
200
|
-
RemoteFile(file_path=file, working_directory=directory)
|
|
201
|
-
for file, _, _ in matching_files
|
|
202
|
-
]
|
|
203
|
-
|
|
204
|
-
return GetMatchingFilesResponse(
|
|
205
|
-
files=files,
|
|
206
|
-
correlation_id=search_term.correlation_id,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
109
|
async def search_files(
|
|
211
110
|
path_str: str,
|
|
212
111
|
file_pattern: str | None,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""HTTP fetch implementation for remote execution client."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any
|
|
5
4
|
|
|
6
5
|
import httpx
|
|
7
6
|
|
|
8
7
|
from exponent.core.remote_execution.cli_rpc_types import (
|
|
9
|
-
HttpResponse,
|
|
10
8
|
HttpRequest,
|
|
9
|
+
HttpResponse,
|
|
11
10
|
)
|
|
12
11
|
|
|
13
12
|
logger = logging.getLogger(__name__)
|
|
@@ -79,7 +78,7 @@ async def fetch_http_content(http_request: HttpRequest) -> HttpResponse:
|
|
|
79
78
|
)
|
|
80
79
|
|
|
81
80
|
except httpx.RequestError as e:
|
|
82
|
-
error_msg = f"Request error for {http_request.url}: {
|
|
81
|
+
error_msg = f"Request error for {http_request.url}: {e!s}"
|
|
83
82
|
return HttpResponse(
|
|
84
83
|
status_code=None,
|
|
85
84
|
content="",
|
|
@@ -87,7 +86,7 @@ async def fetch_http_content(http_request: HttpRequest) -> HttpResponse:
|
|
|
87
86
|
)
|
|
88
87
|
|
|
89
88
|
except Exception as e:
|
|
90
|
-
error_msg = f"Unexpected error fetching {http_request.url}: {
|
|
89
|
+
error_msg = f"Unexpected error fetching {http_request.url}: {e!s}"
|
|
91
90
|
return HttpResponse(
|
|
92
91
|
status_code=None,
|
|
93
92
|
content="",
|
|
@@ -2,14 +2,11 @@ import getpass
|
|
|
2
2
|
import os
|
|
3
3
|
import platform
|
|
4
4
|
|
|
5
|
-
from anyio import Path as AsyncPath
|
|
6
|
-
|
|
7
5
|
from exponent.core.remote_execution.git import get_git_info
|
|
8
6
|
from exponent.core.remote_execution.languages import python_execution
|
|
9
7
|
from exponent.core.remote_execution.types import (
|
|
10
8
|
SystemInfo,
|
|
11
9
|
)
|
|
12
|
-
from exponent.core.remote_execution.utils import safe_read_file
|
|
13
10
|
|
|
14
11
|
|
|
15
12
|
async def get_system_info(working_directory: str) -> SystemInfo:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import uuid
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from pathlib import Path
|
|
@@ -36,9 +37,12 @@ from exponent.core.remote_execution.types import (
|
|
|
36
37
|
)
|
|
37
38
|
from exponent.core.remote_execution.utils import (
|
|
38
39
|
assert_unreachable,
|
|
40
|
+
safe_get_file_metadata,
|
|
39
41
|
safe_read_file,
|
|
40
42
|
)
|
|
41
43
|
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
42
46
|
|
|
43
47
|
async def execute_tool(
|
|
44
48
|
tool_input: ToolInputType, working_directory: str
|
|
@@ -116,9 +120,11 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
116
120
|
return ErrorToolResult(
|
|
117
121
|
error_message="File appears to be binary or has invalid text encoding"
|
|
118
122
|
)
|
|
119
|
-
except Exception as e:
|
|
123
|
+
except Exception as e:
|
|
120
124
|
return ErrorToolResult(error_message=f"Error reading file: {e!s}")
|
|
121
125
|
|
|
126
|
+
metadata = await safe_get_file_metadata(file)
|
|
127
|
+
|
|
122
128
|
# Handle empty files
|
|
123
129
|
if not content:
|
|
124
130
|
return ReadToolResult(
|
|
@@ -126,6 +132,7 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
126
132
|
num_lines=0,
|
|
127
133
|
start_line=0,
|
|
128
134
|
total_lines=0,
|
|
135
|
+
metadata=metadata,
|
|
129
136
|
)
|
|
130
137
|
|
|
131
138
|
content_lines = content.splitlines(keepends=True)
|
|
@@ -138,6 +145,7 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
138
145
|
num_lines=0,
|
|
139
146
|
start_line=offset,
|
|
140
147
|
total_lines=total_lines,
|
|
148
|
+
metadata=metadata,
|
|
141
149
|
)
|
|
142
150
|
|
|
143
151
|
# Apply offset and limit
|
|
@@ -180,6 +188,7 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
180
188
|
num_lines=num_lines,
|
|
181
189
|
start_line=offset,
|
|
182
190
|
total_lines=total_lines,
|
|
191
|
+
metadata=metadata,
|
|
183
192
|
)
|
|
184
193
|
|
|
185
194
|
|
|
@@ -213,6 +222,16 @@ async def execute_edit_file( # noqa: PLR0911
|
|
|
213
222
|
if not exists:
|
|
214
223
|
return ErrorToolResult(error_message="File not found")
|
|
215
224
|
|
|
225
|
+
if tool_input.last_known_modified_timestamp is not None:
|
|
226
|
+
metadata = await safe_get_file_metadata(file)
|
|
227
|
+
if (
|
|
228
|
+
metadata is not None
|
|
229
|
+
and metadata.modified_timestamp > tool_input.last_known_modified_timestamp
|
|
230
|
+
):
|
|
231
|
+
return ErrorToolResult(
|
|
232
|
+
error_message="File has been modified since last read/write"
|
|
233
|
+
)
|
|
234
|
+
|
|
216
235
|
try:
|
|
217
236
|
if await file.is_dir():
|
|
218
237
|
return ErrorToolResult(
|
|
@@ -232,7 +251,7 @@ async def execute_edit_file( # noqa: PLR0911
|
|
|
232
251
|
return ErrorToolResult(
|
|
233
252
|
error_message="File appears to be binary or has invalid text encoding"
|
|
234
253
|
)
|
|
235
|
-
except Exception as e:
|
|
254
|
+
except Exception as e:
|
|
236
255
|
return ErrorToolResult(error_message=f"Error reading file: {e!s}")
|
|
237
256
|
|
|
238
257
|
# Check if search text exists
|
|
@@ -265,9 +284,10 @@ async def execute_edit_file( # noqa: PLR0911
|
|
|
265
284
|
path = Path(working_directory, tool_input.file_path)
|
|
266
285
|
await execute_full_file_rewrite(path, new_content, working_directory)
|
|
267
286
|
return EditToolResult(
|
|
268
|
-
message=f"Successfully replaced text in {tool_input.file_path}"
|
|
287
|
+
message=f"Successfully replaced text in {tool_input.file_path}",
|
|
288
|
+
metadata=await safe_get_file_metadata(path),
|
|
269
289
|
)
|
|
270
|
-
except Exception as e:
|
|
290
|
+
except Exception as e:
|
|
271
291
|
return ErrorToolResult(error_message=f"Error writing file: {e!s}")
|
|
272
292
|
|
|
273
293
|
|
|
@@ -123,6 +123,56 @@ class CompositeTruncation(TruncationStrategy):
|
|
|
123
123
|
return result
|
|
124
124
|
|
|
125
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
|
+
|
|
126
176
|
class StringListTruncation(TruncationStrategy):
|
|
127
177
|
"""Truncation for lists of strings that limits both number of items and individual string length."""
|
|
128
178
|
|
|
@@ -215,7 +265,7 @@ class StringListTruncation(TruncationStrategy):
|
|
|
215
265
|
TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
|
|
216
266
|
ReadToolResult: StringFieldTruncation("content"),
|
|
217
267
|
WriteToolResult: StringFieldTruncation("message"),
|
|
218
|
-
BashToolResult:
|
|
268
|
+
BashToolResult: TailTruncation("shell_output"),
|
|
219
269
|
GrepToolResult: StringListTruncation("matches"),
|
|
220
270
|
GlobToolResult: StringListTruncation("filenames", max_item_length=4096),
|
|
221
271
|
ListToolResult: StringListTruncation("files", max_item_length=4096),
|
|
@@ -236,49 +286,3 @@ def truncate_tool_result(result: T) -> T:
|
|
|
236
286
|
return cast(T, strategy.truncate(result))
|
|
237
287
|
|
|
238
288
|
return result
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def register_truncation_strategy(
|
|
242
|
-
result_type: type[ToolResult],
|
|
243
|
-
strategy: TruncationStrategy,
|
|
244
|
-
) -> None:
|
|
245
|
-
TRUNCATION_REGISTRY[result_type] = strategy
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def configure_truncation_limits(
|
|
249
|
-
character_limit: int | None = None,
|
|
250
|
-
list_item_limit: int | None = None,
|
|
251
|
-
list_preview_items: int | None = None,
|
|
252
|
-
string_item_length: int | None = None,
|
|
253
|
-
) -> None:
|
|
254
|
-
for strategy in TRUNCATION_REGISTRY.values():
|
|
255
|
-
if isinstance(strategy, StringFieldTruncation) and character_limit is not None:
|
|
256
|
-
strategy.character_limit = character_limit
|
|
257
|
-
elif isinstance(strategy, ListFieldTruncation):
|
|
258
|
-
if list_item_limit is not None:
|
|
259
|
-
strategy.item_limit = list_item_limit
|
|
260
|
-
if list_preview_items is not None:
|
|
261
|
-
strategy.preview_items = list_preview_items
|
|
262
|
-
elif isinstance(strategy, StringListTruncation):
|
|
263
|
-
if list_item_limit is not None:
|
|
264
|
-
strategy.max_items = list_item_limit
|
|
265
|
-
if list_preview_items is not None:
|
|
266
|
-
strategy.preview_items = list_preview_items
|
|
267
|
-
if string_item_length is not None:
|
|
268
|
-
strategy.max_item_length = string_item_length
|
|
269
|
-
elif isinstance(strategy, CompositeTruncation):
|
|
270
|
-
for sub_strategy in strategy.strategies:
|
|
271
|
-
if isinstance(sub_strategy, StringFieldTruncation) and character_limit:
|
|
272
|
-
sub_strategy.character_limit = character_limit
|
|
273
|
-
elif isinstance(sub_strategy, ListFieldTruncation):
|
|
274
|
-
if list_item_limit is not None:
|
|
275
|
-
sub_strategy.item_limit = list_item_limit
|
|
276
|
-
if list_preview_items is not None:
|
|
277
|
-
sub_strategy.preview_items = list_preview_items
|
|
278
|
-
elif isinstance(sub_strategy, StringListTruncation):
|
|
279
|
-
if list_item_limit is not None:
|
|
280
|
-
sub_strategy.max_items = list_item_limit
|
|
281
|
-
if list_preview_items is not None:
|
|
282
|
-
sub_strategy.preview_items = list_preview_items
|
|
283
|
-
if string_item_length is not None:
|
|
284
|
-
sub_strategy.max_item_length = string_item_length
|