indent 0.1.20__tar.gz → 0.1.21__tar.gz

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.

Files changed (51) hide show
  1. {indent-0.1.20 → indent-0.1.21}/.gitignore +1 -2
  2. {indent-0.1.20 → indent-0.1.21}/PKG-INFO +1 -1
  3. {indent-0.1.20 → indent-0.1.21}/exponent/__init__.py +2 -2
  4. {indent-0.1.20 → indent-0.1.21}/exponent/commands/cloud_commands.py +0 -84
  5. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/mutations.py +0 -29
  6. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/cli_rpc_types.py +23 -13
  7. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/client.py +47 -33
  8. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/languages/shell_streaming.py +1 -1
  9. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/tool_execution.py +7 -5
  10. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/truncation.py +0 -2
  11. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/types.py +23 -1
  12. {indent-0.1.20 → indent-0.1.21}/exponent/cli.py +0 -0
  13. {indent-0.1.20 → indent-0.1.21}/exponent/commands/common.py +0 -0
  14. {indent-0.1.20 → indent-0.1.21}/exponent/commands/config_commands.py +0 -0
  15. {indent-0.1.20 → indent-0.1.21}/exponent/commands/run_commands.py +0 -0
  16. {indent-0.1.20 → indent-0.1.21}/exponent/commands/settings.py +0 -0
  17. {indent-0.1.20 → indent-0.1.21}/exponent/commands/types.py +0 -0
  18. {indent-0.1.20 → indent-0.1.21}/exponent/commands/upgrade.py +0 -0
  19. {indent-0.1.20 → indent-0.1.21}/exponent/commands/utils.py +0 -0
  20. {indent-0.1.20 → indent-0.1.21}/exponent/core/config.py +0 -0
  21. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/__init__.py +0 -0
  22. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/client.py +0 -0
  23. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/get_chats_query.py +0 -0
  24. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/queries.py +0 -0
  25. {indent-0.1.20 → indent-0.1.21}/exponent/core/graphql/subscriptions.py +0 -0
  26. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/checkpoints.py +0 -0
  27. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/code_execution.py +0 -0
  28. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/default_env.py +0 -0
  29. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/error_info.py +0 -0
  30. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/exceptions.py +0 -0
  31. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/file_write.py +0 -0
  32. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/files.py +0 -0
  33. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/git.py +0 -0
  34. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/http_fetch.py +0 -0
  35. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/languages/python_execution.py +0 -0
  36. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/languages/types.py +0 -0
  37. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/session.py +0 -0
  38. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/system_context.py +0 -0
  39. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/tool_type_utils.py +0 -0
  40. {indent-0.1.20 → indent-0.1.21}/exponent/core/remote_execution/utils.py +0 -0
  41. {indent-0.1.20 → indent-0.1.21}/exponent/core/types/__init__.py +0 -0
  42. {indent-0.1.20 → indent-0.1.21}/exponent/core/types/command_data.py +0 -0
  43. {indent-0.1.20 → indent-0.1.21}/exponent/core/types/event_types.py +0 -0
  44. {indent-0.1.20 → indent-0.1.21}/exponent/core/types/generated/__init__.py +0 -0
  45. {indent-0.1.20 → indent-0.1.21}/exponent/core/types/generated/strategy_info.py +0 -0
  46. {indent-0.1.20 → indent-0.1.21}/exponent/migration-docs/login.md +0 -0
  47. {indent-0.1.20 → indent-0.1.21}/exponent/py.typed +0 -0
  48. {indent-0.1.20 → indent-0.1.21}/exponent/utils/__init__.py +0 -0
  49. {indent-0.1.20 → indent-0.1.21}/exponent/utils/colors.py +0 -0
  50. {indent-0.1.20 → indent-0.1.21}/exponent/utils/version.py +0 -0
  51. {indent-0.1.20 → indent-0.1.21}/pyproject.toml +0 -0
@@ -15,6 +15,7 @@ workflow_input.json
15
15
  CLAUDE.local.md
16
16
  indent.local.md
17
17
  plan.md
18
+ AGENTS.md
18
19
 
19
20
  # C extensions
20
21
  *.so
@@ -164,8 +165,6 @@ dmypy.json
164
165
  # Aider
165
166
  .aider*
166
167
 
167
- # Debug logs
168
- debug/
169
168
 
170
169
  # Local test scripts
171
170
  local_test_scripts/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.20
3
+ Version: 0.1.21
4
4
  Summary: Indent is an AI Pair Programmer
5
5
  Author-email: Sashank Thupukari <sashank@exponent.run>
6
6
  Requires-Python: <3.13,>=3.10
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.20'
32
- __version_tuple__ = version_tuple = (0, 1, 20)
31
+ __version__ = version = '0.1.21'
32
+ __version_tuple__ = version_tuple = (0, 1, 21)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -23,7 +23,6 @@ from exponent.core.graphql.client import GraphQLClient
23
23
  from exponent.core.graphql.mutations import (
24
24
  CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION,
25
25
  ENABLE_CLOUD_REPOSITORY_MUTATION,
26
- INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
27
26
  REBUILD_CLOUD_REPOSITORY_MUTATION,
28
27
  START_CHAT_TURN_MUTATION,
29
28
  )
@@ -62,32 +61,6 @@ async def enable_cloud_repository(
62
61
  return cast(dict[str, Any], result["enableCloudRepository"])
63
62
 
64
63
 
65
- async def incremental_build_cloud_repository(
66
- api_key: str,
67
- base_api_url: str,
68
- base_ws_url: str,
69
- org_name: str,
70
- repo_name: str,
71
- ) -> dict[str, Any]:
72
- graphql_client = GraphQLClient(
73
- api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
74
- )
75
-
76
- variables = {
77
- "orgName": org_name,
78
- "repoName": repo_name,
79
- }
80
-
81
- result = await graphql_client.execute(
82
- INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
83
- variables,
84
- "IncrementalBuildCloudRepository",
85
- timeout=120,
86
- )
87
-
88
- return cast(dict[str, Any], result["incrementalBuildCloudRepository"])
89
-
90
-
91
64
  async def rebuild_cloud_repository(
92
65
  api_key: str,
93
66
  base_api_url: str,
@@ -243,63 +216,6 @@ def enable_repo(
243
216
  sys.exit(1)
244
217
 
245
218
 
246
- @cloud_cli.command(hidden=True)
247
- @click.option(
248
- "--org-name",
249
- help="GitHub organization name",
250
- required=True,
251
- )
252
- @click.option(
253
- "--repo-name",
254
- help="GitHub repository name",
255
- required=True,
256
- )
257
- @use_settings
258
- def incremental_build(
259
- settings: Settings,
260
- org_name: str,
261
- repo_name: str,
262
- ) -> None:
263
- """Test utility for incremental build of cloud repository."""
264
- check_exponent_version_and_upgrade(settings)
265
-
266
- if not settings.api_key:
267
- redirect_to_login(settings)
268
- return
269
-
270
- loop = asyncio.get_event_loop()
271
-
272
- api_key = settings.api_key
273
- base_api_url = settings.get_base_api_url()
274
- base_ws_url = settings.get_base_ws_url()
275
-
276
- try:
277
- result = loop.run_until_complete(
278
- incremental_build_cloud_repository(
279
- api_key, base_api_url, base_ws_url, org_name, repo_name
280
- )
281
- )
282
-
283
- if result["__typename"] == "ContainerImage":
284
- click.secho(
285
- f"✓ Successfully triggered incremental build for {org_name}/{repo_name}",
286
- fg="green",
287
- )
288
- click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
289
- click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
290
- click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
291
- else:
292
- click.secho(
293
- f"✗ Failed to trigger incremental build: {result.get('message', 'Unknown error')}",
294
- fg="red",
295
- )
296
- click.echo(f" Error type: {result['__typename']}")
297
-
298
- except Exception as e:
299
- click.secho(f"✗ Error triggering incremental build: {e!s}", fg="red")
300
- sys.exit(1)
301
-
302
-
303
219
  @cloud_cli.command(hidden=True)
304
220
  @click.option(
305
221
  "--org-name",
@@ -131,35 +131,6 @@ mutation EnableCloudRepository($orgName: String!, $repoName: String!) {
131
131
  """
132
132
 
133
133
 
134
- INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION = """
135
- mutation IncrementalBuildCloudRepository($orgName: String!, $repoName: String!) {
136
- incrementalBuildCloudRepository(orgName: $orgName, repoName: $repoName) {
137
- __typename
138
- ...on ContainerImage {
139
- buildRef
140
- createdAt
141
- updatedAt
142
- }
143
- ...on UnauthenticatedError {
144
- message
145
- }
146
- ...on CloudConfigNotFoundError {
147
- message
148
- }
149
- ...on GithubConfigNotFoundError {
150
- message
151
- }
152
- ...on CloudSessionError {
153
- message
154
- }
155
- ...on Error {
156
- message
157
- }
158
- }
159
- }
160
- """
161
-
162
-
163
134
  REBUILD_CLOUD_REPOSITORY_MUTATION = """
164
135
  mutation RebuildCloudRepository($orgName: String!, $repoName: String!) {
165
136
  rebuildCloudRepository(orgName: $orgName, repoName: $repoName) {
@@ -66,14 +66,28 @@ class FileMetadata(msgspec.Struct):
66
66
  file_mode: str
67
67
 
68
68
 
69
+ class ReadToolArtifactResult(ToolResult, tag=READ_TOOL_ARTIFACT_NAME):
70
+ s3_uri: str
71
+ file_path: str
72
+ media_type: str
73
+
74
+ def to_text(self) -> str:
75
+ return f"[Image artifact uploaded to {self.s3_uri}]"
76
+
77
+
69
78
  class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
70
- content: str
71
- num_lines: int
72
- start_line: int
73
- total_lines: int
79
+ content: str | None = None
80
+ num_lines: int | None = None
81
+ start_line: int | None = None
82
+ total_lines: int | None = None
74
83
  metadata: FileMetadata | None = None
84
+ artifact: ReadToolArtifactResult | None = None
75
85
 
76
86
  def to_text(self) -> str:
87
+ if self.artifact:
88
+ return self.artifact.to_text()
89
+ assert self.content is not None
90
+ assert self.start_line is not None
77
91
  lines = self.content.splitlines()
78
92
  lines = [
79
93
  f"{str(i).rjust(6)}→{line}"
@@ -82,15 +96,6 @@ class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
82
96
  return "\n".join(lines)
83
97
 
84
98
 
85
- class ReadToolArtifactResult(ToolResult, tag=READ_TOOL_ARTIFACT_NAME):
86
- s3_uri: str
87
- file_path: str
88
- media_type: str
89
-
90
- def to_text(self) -> str:
91
- return f"[Image artifact uploaded to {self.s3_uri}]"
92
-
93
-
94
99
  LIST_TOOL_NAME = "ls"
95
100
 
96
101
 
@@ -273,6 +278,10 @@ class TerminateResponse(msgspec.Struct, tag="terminate"):
273
278
  pass
274
279
 
275
280
 
281
+ class TimeoutResponse(msgspec.Struct, tag="timeout"):
282
+ pass
283
+
284
+
276
285
  class BatchToolExecutionResponse(msgspec.Struct, tag="batch_tool_execution"):
277
286
  tool_results: list[ToolResultType]
278
287
 
@@ -324,6 +333,7 @@ class CliRpcResponse(msgspec.Struct):
324
333
  | GetAllFilesResponse
325
334
  | ErrorResponse
326
335
  | TerminateResponse
336
+ | TimeoutResponse
327
337
  | BatchToolExecutionResponse
328
338
  | HttpResponse
329
339
  | SwitchCLIChatResponse
@@ -413,6 +413,7 @@ class RemoteExecutionClient:
413
413
  results: asyncio.Queue[CliRpcResponse],
414
414
  ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
415
415
  """Process messages from the websocket connection."""
416
+ pending: set[asyncio.Task[object]] = set()
416
417
  try:
417
418
  recv = asyncio.create_task(websocket.recv())
418
419
  get_beat = asyncio.create_task(beats.get())
@@ -462,6 +463,9 @@ class RemoteExecutionClient:
462
463
  self,
463
464
  websocket: ClientConnection,
464
465
  connection_tracker: ConnectionTracker | None,
466
+ beats: asyncio.Queue[HeartbeatInfo],
467
+ requests: asyncio.Queue[CliRpcRequest],
468
+ results: asyncio.Queue[CliRpcResponse],
465
469
  ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO | None:
466
470
  """Handle a single websocket connection.
467
471
  Returns None to continue with reconnection attempts, or an exit info to terminate."""
@@ -470,12 +474,6 @@ class RemoteExecutionClient:
470
474
 
471
475
  self._websocket = websocket
472
476
 
473
- beats: asyncio.Queue[HeartbeatInfo] = asyncio.Queue()
474
- requests: asyncio.Queue[CliRpcRequest] = asyncio.Queue()
475
- results: asyncio.Queue[CliRpcResponse] = asyncio.Queue()
476
-
477
- tasks = await self._setup_tasks(beats, requests, results)
478
-
479
477
  try:
480
478
  return await self._process_websocket_messages(
481
479
  websocket, beats, requests, results
@@ -493,15 +491,13 @@ class RemoteExecutionClient:
493
491
  )
494
492
  return WSDisconnected(error_message=error_message)
495
493
  # Otherwise, allow reconnection attempt
494
+ logger.debug("Websocket connection closed by remote.")
496
495
  return None
497
496
  except TimeoutError:
498
497
  # Timeout, allow reconnection attempt
499
498
  # TODO: investgate if this is needed, possibly scope it down
500
499
  return None
501
500
  finally:
502
- for task in tasks:
503
- task.cancel()
504
- await asyncio.gather(*tasks, return_exceptions=True)
505
501
  if connection_tracker is not None:
506
502
  await connection_tracker.set_connected(False)
507
503
 
@@ -517,33 +513,51 @@ class RemoteExecutionClient:
517
513
  # Initialize last request time for timeout monitoring
518
514
  self._last_request_time = time.time()
519
515
 
520
- async for websocket in self.ws_connect(f"/api/ws/chat/{chat_uuid}"):
521
- # Always run connection and timeout monitor concurrently
522
- # If timeout_seconds is None, timeout monitor will loop indefinitely
523
- done, pending = await asyncio.wait(
524
- [
525
- asyncio.create_task(
526
- self._handle_websocket_connection(websocket, connection_tracker)
527
- ),
528
- asyncio.create_task(self._timeout_monitor(timeout_seconds)),
529
- ],
530
- return_when=asyncio.FIRST_COMPLETED,
531
- )
516
+ # Create queues ONCE - persist across reconnections
517
+ beats: asyncio.Queue[HeartbeatInfo] = asyncio.Queue()
518
+ requests: asyncio.Queue[CliRpcRequest] = asyncio.Queue()
519
+ results: asyncio.Queue[CliRpcResponse] = asyncio.Queue()
532
520
 
533
- # Cancel pending tasks
534
- for task in pending:
535
- task.cancel()
521
+ # Create tasks ONCE - persist across reconnections
522
+ executors = await self._setup_tasks(beats, requests, results)
536
523
 
537
- # Return result from completed task
538
- for task in done:
539
- result = await task
540
- # If we get None, we'll try to reconnect
541
- if result is not None:
542
- return result
524
+ try:
525
+ async for websocket in self.ws_connect(f"/api/ws/chat/{chat_uuid}"):
526
+ # Always run connection and timeout monitor concurrently
527
+ # If timeout_seconds is None, timeout monitor will loop indefinitely
528
+ done, pending = await asyncio.wait(
529
+ [
530
+ asyncio.create_task(
531
+ self._handle_websocket_connection(
532
+ websocket, connection_tracker, beats, requests, results
533
+ )
534
+ ),
535
+ asyncio.create_task(self._timeout_monitor(timeout_seconds)),
536
+ ],
537
+ return_when=asyncio.FIRST_COMPLETED,
538
+ )
543
539
 
544
- # If we exit the websocket connection loop without returning,
545
- # it means we couldn't establish a connection
546
- return WSDisconnected(error_message="Could not establish websocket connection")
540
+ # Cancel pending tasks
541
+ for task in pending:
542
+ task.cancel()
543
+
544
+ # Return result from completed task
545
+ for task in done:
546
+ result = await task
547
+ # If we get None, we'll try to reconnect
548
+ if result is not None:
549
+ return result
550
+
551
+ # If we exit the websocket connection loop without returning,
552
+ # it means we couldn't establish a connection
553
+ return WSDisconnected(
554
+ error_message="Could not establish websocket connection"
555
+ )
556
+ finally:
557
+ # Cancel all background tasks when exiting
558
+ for task in executors:
559
+ task.cancel()
560
+ await asyncio.gather(*executors, return_exceptions=True)
547
561
 
548
562
  async def create_chat(self, chat_source: ChatSource) -> CreateChatResponse:
549
563
  response = await self.api_client.post(
@@ -53,7 +53,7 @@ async def read_stream(
53
53
  data = await stream.read(4096)
54
54
  if not data:
55
55
  break
56
- chunk = data.decode(encoding=encoding)
56
+ chunk = data.decode(encoding=encoding, errors="replace")
57
57
  output.append((fd, chunk))
58
58
  yield StreamedOutputPiece(content=chunk)
59
59
  except UnicodeDecodeError:
@@ -90,7 +90,7 @@ async def execute_read_file( # noqa: PLR0911, PLR0915
90
90
  tool_input: ReadToolInput,
91
91
  working_directory: str,
92
92
  upload_client: "RemoteExecutionClient | None" = None,
93
- ) -> ReadToolResult | ReadToolArtifactResult | ErrorToolResult:
93
+ ) -> ReadToolResult | ErrorToolResult:
94
94
  # Validate absolute path requirement
95
95
  if not tool_input.file_path.startswith("/"):
96
96
  return ErrorToolResult(
@@ -136,10 +136,12 @@ async def execute_read_file( # noqa: PLR0911, PLR0915
136
136
  if status != 200:
137
137
  raise RuntimeError(f"Upload failed with status {status}")
138
138
 
139
- return ReadToolArtifactResult(
140
- s3_uri=upload_response.s3_uri,
141
- file_path=tool_input.file_path,
142
- media_type=media_type,
139
+ return ReadToolResult(
140
+ artifact=ReadToolArtifactResult(
141
+ s3_uri=upload_response.s3_uri,
142
+ file_path=tool_input.file_path,
143
+ media_type=media_type,
144
+ )
143
145
  )
144
146
  except Exception as e:
145
147
  return ErrorToolResult(error_message=f"Failed to upload image to S3: {e!s}")
@@ -11,7 +11,6 @@ from exponent.core.remote_execution.cli_rpc_types import (
11
11
  GlobToolResult,
12
12
  GrepToolResult,
13
13
  ListToolResult,
14
- ReadToolArtifactResult,
15
14
  ReadToolResult,
16
15
  ToolResult,
17
16
  WriteToolResult,
@@ -273,7 +272,6 @@ class StringListTruncation(TruncationStrategy):
273
272
 
274
273
  TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
275
274
  ReadToolResult: StringFieldTruncation("content"),
276
- ReadToolArtifactResult: NoOpTruncation(),
277
275
  WriteToolResult: StringFieldTruncation("message"),
278
276
  BashToolResult: TailTruncation("shell_output"),
279
277
  GrepToolResult: StringListTruncation("matches"),
@@ -50,6 +50,7 @@ class PrReviewWorkflowInput(BaseModel):
50
50
 
51
51
 
52
52
  class SlackWorkflowInput(BaseModel):
53
+ discriminator: Literal["slack_workflow"] = "slack_workflow"
53
54
  channel_id: str
54
55
  thread_ts: str
55
56
  slack_url: str | None = None
@@ -57,13 +58,34 @@ class SlackWorkflowInput(BaseModel):
57
58
  message_ts: str | None = None
58
59
 
59
60
 
61
+ class SlackPlanApprovalWorkflowInput(BaseModel):
62
+ discriminator: Literal["slack_plan_approval"] = "slack_plan_approval"
63
+ channel_id: str
64
+ thread_ts: str
65
+ slack_url: str
66
+ channel_name: str
67
+ message_ts: str
68
+
69
+
60
70
  class SentryWorkflowInput(BaseModel):
61
71
  title: str
62
72
  issue_id: str
63
73
  permalink: str
64
74
 
65
75
 
66
- WorkflowInput = PrReviewWorkflowInput | SlackWorkflowInput | SentryWorkflowInput
76
+ class GenericCloudWorkflowInput(BaseModel):
77
+ initial_prompt: str
78
+ system_prompt_override: str | None = None
79
+ reasoning_level: str = "LOW"
80
+
81
+
82
+ WorkflowInput = (
83
+ PrReviewWorkflowInput
84
+ | SlackWorkflowInput
85
+ | SentryWorkflowInput
86
+ | GenericCloudWorkflowInput
87
+ | SlackPlanApprovalWorkflowInput
88
+ )
67
89
 
68
90
 
69
91
  class WorkflowTriggerRequest(BaseModel):
File without changes
File without changes
File without changes
File without changes