indent 0.1.19__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 (52) hide show
  1. {indent-0.1.19 → indent-0.1.21}/.gitignore +4 -2
  2. {indent-0.1.19 → indent-0.1.21}/PKG-INFO +1 -1
  3. {indent-0.1.19 → indent-0.1.21}/exponent/__init__.py +2 -2
  4. {indent-0.1.19 → indent-0.1.21}/exponent/commands/cloud_commands.py +0 -84
  5. {indent-0.1.19 → indent-0.1.21}/exponent/commands/config_commands.py +0 -87
  6. {indent-0.1.19 → indent-0.1.21}/exponent/commands/run_commands.py +1 -1
  7. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/mutations.py +0 -29
  8. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/cli_rpc_types.py +24 -13
  9. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/client.py +47 -33
  10. indent-0.1.21/exponent/core/remote_execution/default_env.py +31 -0
  11. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/languages/shell_streaming.py +5 -1
  12. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/tool_execution.py +7 -5
  13. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/truncation.py +0 -2
  14. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/types.py +30 -1
  15. indent-0.1.19/exponent/core/graphql/github_config_queries.py +0 -56
  16. {indent-0.1.19 → indent-0.1.21}/exponent/cli.py +0 -0
  17. {indent-0.1.19 → indent-0.1.21}/exponent/commands/common.py +0 -0
  18. {indent-0.1.19 → indent-0.1.21}/exponent/commands/settings.py +0 -0
  19. {indent-0.1.19 → indent-0.1.21}/exponent/commands/types.py +0 -0
  20. {indent-0.1.19 → indent-0.1.21}/exponent/commands/upgrade.py +0 -0
  21. {indent-0.1.19 → indent-0.1.21}/exponent/commands/utils.py +0 -0
  22. {indent-0.1.19 → indent-0.1.21}/exponent/core/config.py +0 -0
  23. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/__init__.py +0 -0
  24. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/client.py +0 -0
  25. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/get_chats_query.py +0 -0
  26. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/queries.py +0 -0
  27. {indent-0.1.19 → indent-0.1.21}/exponent/core/graphql/subscriptions.py +0 -0
  28. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/checkpoints.py +0 -0
  29. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/code_execution.py +0 -0
  30. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/error_info.py +0 -0
  31. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/exceptions.py +0 -0
  32. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/file_write.py +0 -0
  33. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/files.py +0 -0
  34. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/git.py +0 -0
  35. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/http_fetch.py +0 -0
  36. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/languages/python_execution.py +0 -0
  37. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/languages/types.py +0 -0
  38. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/session.py +0 -0
  39. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/system_context.py +0 -0
  40. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/tool_type_utils.py +0 -0
  41. {indent-0.1.19 → indent-0.1.21}/exponent/core/remote_execution/utils.py +0 -0
  42. {indent-0.1.19 → indent-0.1.21}/exponent/core/types/__init__.py +0 -0
  43. {indent-0.1.19 → indent-0.1.21}/exponent/core/types/command_data.py +0 -0
  44. {indent-0.1.19 → indent-0.1.21}/exponent/core/types/event_types.py +0 -0
  45. {indent-0.1.19 → indent-0.1.21}/exponent/core/types/generated/__init__.py +0 -0
  46. {indent-0.1.19 → indent-0.1.21}/exponent/core/types/generated/strategy_info.py +0 -0
  47. {indent-0.1.19 → indent-0.1.21}/exponent/migration-docs/login.md +0 -0
  48. {indent-0.1.19 → indent-0.1.21}/exponent/py.typed +0 -0
  49. {indent-0.1.19 → indent-0.1.21}/exponent/utils/__init__.py +0 -0
  50. {indent-0.1.19 → indent-0.1.21}/exponent/utils/colors.py +0 -0
  51. {indent-0.1.19 → indent-0.1.21}/exponent/utils/version.py +0 -0
  52. {indent-0.1.19 → 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,9 @@ dmypy.json
164
165
  # Aider
165
166
  .aider*
166
167
 
167
- # Debug logs
168
- debug/
168
+
169
+ # Local test scripts
170
+ local_test_scripts/
169
171
 
170
172
  # Benchmarks
171
173
  .benchy.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.19
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.19'
32
- __version_tuple__ = version_tuple = (0, 1, 19)
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",
@@ -14,11 +14,6 @@ from exponent.commands.types import exponent_cli_group
14
14
  from exponent.core.config import Settings
15
15
  from exponent.core.graphql.client import GraphQLClient
16
16
  from exponent.core.graphql.get_chats_query import GET_CHATS_QUERY
17
- from exponent.core.graphql.github_config_queries import (
18
- CHECK_GITHUB_CONFIG_VALIDITY_QUERY,
19
- CREATE_GITHUB_CONFIG_MUTATION,
20
- REPOS_FOR_GITHUB_CONFIG_QUERY,
21
- )
22
17
  from exponent.core.graphql.subscriptions import AUTHENTICATED_USER_SUBSCRIPTION
23
18
  from exponent.utils.version import (
24
19
  get_installed_metadata,
@@ -56,74 +51,6 @@ def debug(
56
51
  click.echo(get_installed_metadata())
57
52
 
58
53
 
59
- @config_cli.command(hidden=True)
60
- @use_settings
61
- def check_github_config_validity(
62
- settings: Settings,
63
- ) -> None:
64
- if not settings.api_key:
65
- redirect_to_login(settings)
66
- return
67
-
68
- run_until_complete(
69
- check_github_config_validity_task(
70
- api_key=settings.api_key,
71
- base_api_url=settings.get_base_api_url(),
72
- base_ws_url=settings.get_base_ws_url(),
73
- )
74
- )
75
-
76
-
77
- async def check_github_config_validity_task(
78
- api_key: str,
79
- base_api_url: str,
80
- base_ws_url: str,
81
- ) -> None:
82
- graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
83
- result = await graphql_client.execute(CHECK_GITHUB_CONFIG_VALIDITY_QUERY)
84
- click.echo(result)
85
-
86
-
87
- @config_cli.command(hidden=True)
88
- @use_settings
89
- def repos_for_github_config(
90
- settings: Settings,
91
- ) -> None:
92
- if not settings.api_key:
93
- redirect_to_login(settings)
94
- return
95
-
96
- run_until_complete(
97
- repos_for_github_config_task(
98
- api_key=settings.api_key,
99
- base_api_url=settings.get_base_api_url(),
100
- base_ws_url=settings.get_base_ws_url(),
101
- )
102
- )
103
-
104
-
105
- async def repos_for_github_config_task(
106
- api_key: str,
107
- base_api_url: str,
108
- base_ws_url: str,
109
- ) -> None:
110
- graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
111
- try:
112
- click.echo("Sending request to fetch repos...")
113
- result = await graphql_client.execute(
114
- REPOS_FOR_GITHUB_CONFIG_QUERY, timeout=120
115
- ) # 120 seconds timeout
116
- click.echo("Request completed. Result:")
117
- click.echo(result)
118
- except Exception as e:
119
- click.echo(f"An error occurred while fetching repos: {e!s}")
120
- click.echo(f"Error type: {type(e).__name__}")
121
- # Add more detailed error information if available
122
- if hasattr(e, "response"):
123
- click.echo(f"Response status: {e.response.status_code}")
124
- click.echo(f"Response content: {e.response.text}")
125
-
126
-
127
54
  @config_cli.command(hidden=True)
128
55
  @click.option(
129
56
  "--set-git-warning-disabled",
@@ -389,20 +316,6 @@ async def get_authenticated_user_task(
389
316
  click.echo(it)
390
317
 
391
318
 
392
- async def create_github_config_task(
393
- api_key: str,
394
- base_api_url: str,
395
- base_ws_url: str,
396
- github_pat: str,
397
- ) -> None:
398
- graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
399
- variables = {
400
- "githubPat": github_pat,
401
- }
402
- result = await graphql_client.execute(CREATE_GITHUB_CONFIG_MUTATION, variables)
403
- click.echo(result)
404
-
405
-
406
319
  @config_cli.command(hidden=True)
407
320
  @use_settings
408
321
  def refresh_key(settings: Settings) -> None:
@@ -97,7 +97,7 @@ def run(
97
97
  create_chat(api_key, base_api_url, base_ws_url, ChatSource.CLI_RUN)
98
98
  )
99
99
 
100
- if timeout_seconds is not None and timeout_seconds <= 0:
100
+ if isinstance(timeout_seconds, int) and timeout_seconds <= 0:
101
101
  click.secho("Error: --timeout-seconds must be a positive integer", fg="red")
102
102
  sys.exit(1)
103
103
 
@@ -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) {
@@ -48,6 +48,7 @@ class ToolResult(msgspec.Struct, tag_field="tool_name", omit_defaults=True):
48
48
 
49
49
  class ErrorToolResult(ToolResult, tag="error"):
50
50
  error_message: str
51
+ is_assistant_error: bool = False
51
52
 
52
53
 
53
54
  READ_TOOL_NAME = "read"
@@ -65,14 +66,28 @@ class FileMetadata(msgspec.Struct):
65
66
  file_mode: str
66
67
 
67
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
+
68
78
  class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
69
- content: str
70
- num_lines: int
71
- start_line: int
72
- 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
73
83
  metadata: FileMetadata | None = None
84
+ artifact: ReadToolArtifactResult | None = None
74
85
 
75
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
76
91
  lines = self.content.splitlines()
77
92
  lines = [
78
93
  f"{str(i).rjust(6)}→{line}"
@@ -81,15 +96,6 @@ class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
81
96
  return "\n".join(lines)
82
97
 
83
98
 
84
- class ReadToolArtifactResult(ToolResult, tag=READ_TOOL_ARTIFACT_NAME):
85
- s3_uri: str
86
- file_path: str
87
- media_type: str
88
-
89
- def to_text(self) -> str:
90
- return f"[Image artifact uploaded to {self.s3_uri}]"
91
-
92
-
93
99
  LIST_TOOL_NAME = "ls"
94
100
 
95
101
 
@@ -272,6 +278,10 @@ class TerminateResponse(msgspec.Struct, tag="terminate"):
272
278
  pass
273
279
 
274
280
 
281
+ class TimeoutResponse(msgspec.Struct, tag="timeout"):
282
+ pass
283
+
284
+
275
285
  class BatchToolExecutionResponse(msgspec.Struct, tag="batch_tool_execution"):
276
286
  tool_results: list[ToolResultType]
277
287
 
@@ -323,6 +333,7 @@ class CliRpcResponse(msgspec.Struct):
323
333
  | GetAllFilesResponse
324
334
  | ErrorResponse
325
335
  | TerminateResponse
336
+ | TimeoutResponse
326
337
  | BatchToolExecutionResponse
327
338
  | HttpResponse
328
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(
@@ -0,0 +1,31 @@
1
+ import os
2
+
3
+
4
+ def get_default_env() -> dict[str, str]:
5
+ """
6
+ Returns default environment variables for CLI-spawned processes.
7
+ These are merged with the parent process environment.
8
+ """
9
+ return {
10
+ "GIT_EDITOR": "true",
11
+ }
12
+
13
+
14
+ def get_process_env(env_overrides: dict[str, str] | None = None) -> dict[str, str]:
15
+ """
16
+ Returns the complete environment for spawned processes.
17
+ Merges parent environment with default variables, then applies overrides.
18
+
19
+ Priority order (lowest to highest):
20
+ 1. Parent process environment (os.environ)
21
+ 2. Default environment variables (get_default_env())
22
+ 3. Explicit overrides (env_overrides parameter)
23
+
24
+ Args:
25
+ env_overrides: Optional dict of environment variables that override defaults
26
+ """
27
+ env = os.environ.copy()
28
+ env.update(get_default_env())
29
+ if env_overrides:
30
+ env.update(env_overrides)
31
+ return env
@@ -6,6 +6,7 @@ import signal
6
6
  from collections.abc import AsyncGenerator, Callable
7
7
  from typing import Any
8
8
 
9
+ from exponent.core.remote_execution.default_env import get_process_env
9
10
  from exponent.core.remote_execution.languages.types import (
10
11
  ShellExecutionResult,
11
12
  StreamedOutputPiece,
@@ -52,7 +53,7 @@ async def read_stream(
52
53
  data = await stream.read(4096)
53
54
  if not data:
54
55
  break
55
- chunk = data.decode(encoding=encoding)
56
+ chunk = data.decode(encoding=encoding, errors="replace")
56
57
  output.append((fd, chunk))
57
58
  yield StreamedOutputPiece(content=chunk)
58
59
  except UnicodeDecodeError:
@@ -80,6 +81,7 @@ async def execute_shell_streaming( # noqa: PLR0915
80
81
  working_directory: str,
81
82
  timeout: int,
82
83
  should_halt: Callable[[], bool] | None = None,
84
+ env: dict[str, str] | None = None,
83
85
  ) -> AsyncGenerator[StreamedOutputPiece | ShellExecutionResult, None]:
84
86
  timeout_seconds = min(timeout, MAX_TIMEOUT)
85
87
 
@@ -91,6 +93,7 @@ async def execute_shell_streaming( # noqa: PLR0915
91
93
  stdout=asyncio.subprocess.PIPE,
92
94
  stderr=asyncio.subprocess.PIPE,
93
95
  cwd=working_directory,
96
+ env=get_process_env(env),
94
97
  )
95
98
  else:
96
99
  # Add rc file sourcing to the command
@@ -105,6 +108,7 @@ async def execute_shell_streaming( # noqa: PLR0915
105
108
  stdout=asyncio.subprocess.PIPE,
106
109
  stderr=asyncio.subprocess.PIPE,
107
110
  cwd=working_directory,
111
+ env=get_process_env(env),
108
112
  start_new_session=True if platform.system() != "Windows" else False,
109
113
  )
110
114
 
@@ -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,7 +58,34 @@ class SlackWorkflowInput(BaseModel):
57
58
  message_ts: str | None = None
58
59
 
59
60
 
60
- WorkflowInput = PrReviewWorkflowInput | SlackWorkflowInput
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
+
70
+ class SentryWorkflowInput(BaseModel):
71
+ title: str
72
+ issue_id: str
73
+ permalink: str
74
+
75
+
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
+ )
61
89
 
62
90
 
63
91
  class WorkflowTriggerRequest(BaseModel):
@@ -542,6 +570,7 @@ class ChatSource(str, Enum):
542
570
  DESKTOP_APP = "DESKTOP_APP"
543
571
  VSCODE_EXTENSION = "VSCODE_EXTENSION"
544
572
  SLACK_APP = "SLACK_APP"
573
+ SENTRY_APP = "SENTRY_APP"
545
574
 
546
575
 
547
576
  class CLIConnectedState(BaseModel):
@@ -1,56 +0,0 @@
1
- CREATE_GITHUB_CONFIG_MUTATION: str = """
2
- mutation CreateGithubConfig(
3
- $githubPat: String!,
4
- ) {
5
- createGithubConfig(
6
- githubPat: $githubPat
7
- ) {
8
- __typename
9
- ... on GithubConfig {
10
- githubConfigUuid
11
- githubPat
12
- }
13
- }
14
- }
15
- """
16
-
17
- CHECK_GITHUB_CONFIG_VALIDITY_QUERY: str = """
18
- query CheckGithubConfigValidity {
19
- checkGithubConfigValidity {
20
- __typename
21
- ... on GithubConfigValidityResult {
22
- isValid
23
- message
24
- }
25
- ... on Error {
26
- message
27
- }
28
- }
29
- }
30
- """
31
-
32
- REPOS_FOR_GITHUB_CONFIG_QUERY: str = """
33
- query ReposForGithubConfig {
34
- reposForGithubConfig {
35
- __typename
36
- ... on GithubConfigRepos {
37
- repos {
38
- id
39
- name
40
- fullName
41
- private
42
- owner
43
- description
44
- }
45
- orgs {
46
- login
47
- id
48
- url
49
- }
50
- }
51
- ... on Error {
52
- message
53
- }
54
- }
55
- }
56
- """
File without changes
File without changes
File without changes
File without changes