sovara 0.2.1__tar.gz → 0.2.3__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.
Files changed (136) hide show
  1. {sovara-0.2.1 → sovara-0.2.3}/PKG-INFO +3 -1
  2. {sovara-0.2.1 → sovara-0.2.3}/pyproject.toml +6 -2
  3. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/assets/SKILL.md +34 -0
  4. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_record.py +10 -1
  5. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_server.py +58 -38
  6. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_tool.py +134 -2
  7. sovara-0.2.3/src/sovara/common/runtime.py +15 -0
  8. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/agent_runner.py +43 -5
  9. sovara-0.2.3/src/sovara/runner/run_log_capture.py +205 -0
  10. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/app.py +29 -1
  11. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/runs.py +14 -0
  12. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/runs.py +15 -0
  13. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/inference_server.py +17 -8
  14. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/__init__.py +2 -0
  15. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/cli_handlers.py +19 -0
  16. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/runner_handlers.py +22 -0
  17. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/runner.py +18 -0
  18. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui.py +1 -0
  19. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_runs.py +9 -0
  20. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/state.py +7 -0
  21. sovara-0.2.3/src/sovara/server/web_app_assets.py +68 -0
  22. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/PKG-INFO +3 -1
  23. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/SOURCES.txt +3 -0
  24. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/requires.txt +3 -0
  25. {sovara-0.2.1 → sovara-0.2.3}/LICENSE +0 -0
  26. {sovara-0.2.1 → sovara-0.2.3}/README.md +0 -0
  27. {sovara-0.2.1 → sovara-0.2.3}/docs/release/PYPI_DESC.md +0 -0
  28. {sovara-0.2.1 → sovara-0.2.3}/setup.cfg +0 -0
  29. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/__init__.py +0 -0
  30. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/assets/__init__.py +0 -0
  31. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/__init__.py +0 -0
  32. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/cli/so_config.py +0 -0
  33. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/__init__.py +0 -0
  34. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/config.py +0 -0
  35. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/constants.py +0 -0
  36. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/custom_metrics.py +0 -0
  37. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/logger.py +0 -0
  38. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/project.py +0 -0
  39. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/user.py +0 -0
  40. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/common/utils.py +0 -0
  41. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/__init__.py +0 -0
  42. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/context_manager.py +0 -0
  43. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/__init__.py +0 -0
  44. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parser.py +0 -0
  45. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/__init__.py +0 -0
  46. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/aiohttp_api_parser.py +0 -0
  47. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/claude_sdk_api_parser.py +0 -0
  48. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/httpx_api_parser.py +0 -0
  49. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/mcp_api_parser.py +0 -0
  50. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/requests_api_parser.py +0 -0
  51. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/sse_utils.py +0 -0
  52. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/api_parsers/urllib3_api_parser.py +0 -0
  53. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/apply_monkey_patches.py +0 -0
  54. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/async_streaming.py +0 -0
  55. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/edit_io_filters.py +0 -0
  56. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/endpoint_whitelist.py +0 -0
  57. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/node_naming.py +0 -0
  58. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/__init__.py +0 -0
  59. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/aiohttp_patch.py +0 -0
  60. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/claude_sdk_patch.py +0 -0
  61. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/httpx_patch.py +0 -0
  62. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/mcp_patches.py +0 -0
  63. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/randomness_patch.py +0 -0
  64. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/requests_patch.py +0 -0
  65. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patches/urllib3_patch.py +0 -0
  66. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/monkey_patching/patching_utils.py +0 -0
  67. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/priors.py +0 -0
  68. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/priors_pipeline.py +0 -0
  69. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/runner/string_matching.py +0 -0
  70. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/__init__.py +0 -0
  71. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/__init__.py +0 -0
  72. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/_shared.py +0 -0
  73. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/llm_calls.py +0 -0
  74. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/manager.py +0 -0
  75. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/priors.py +0 -0
  76. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/projects.py +0 -0
  77. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/__init__.py +0 -0
  78. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/connection.py +0 -0
  79. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/llm_calls.py +0 -0
  80. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/priors.py +0 -0
  81. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/projects.py +0 -0
  82. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/schema.py +0 -0
  83. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/sqlite/users.py +0 -0
  84. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/database/users.py +0 -0
  85. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/__init__.py +0 -0
  86. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/cancel.py +0 -0
  87. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/main.py +0 -0
  88. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/__init__.py +0 -0
  89. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/ask_step.py +0 -0
  90. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/edit_content.py +0 -0
  91. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_overview.py +0 -0
  92. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_snapshot.py +0 -0
  93. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/get_trace_overview.py +0 -0
  94. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/search.py +0 -0
  95. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/summarize_trace.py +0 -0
  96. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/tools/verify.py +0 -0
  97. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/__init__.py +0 -0
  98. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/content_utils.py +0 -0
  99. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/context.py +0 -0
  100. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/edit_persist.py +0 -0
  101. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/step_content_view.py +0 -0
  102. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/step_ids.py +0 -0
  103. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_analysis/trace_chat/utils/trace.py +0 -0
  104. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/graph_models.py +0 -0
  105. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/handler_utils.py +0 -0
  106. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/handlers/ui_handlers.py +0 -0
  107. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_backend.py +0 -0
  108. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_provider_catalog.py +0 -0
  109. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/llm_settings.py +0 -0
  110. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/package_version.py +0 -0
  111. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/prior_display.py +0 -0
  112. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/__init__.py +0 -0
  113. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/constants.py +0 -0
  114. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/events.py +0 -0
  115. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/__init__.py +0 -0
  116. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_restructurer.py +0 -0
  117. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_retriever.py +0 -0
  118. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_summarizer.py +0 -0
  119. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm/lesson_validator.py +0 -0
  120. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/llm_client.py +0 -0
  121. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/logger.py +0 -0
  122. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/routes.py +0 -0
  123. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_backend/storage.py +0 -0
  124. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/priors_scope.py +0 -0
  125. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/__init__.py +0 -0
  126. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/events.py +0 -0
  127. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/internal.py +0 -0
  128. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_common.py +0 -0
  129. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_projects.py +0 -0
  130. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/routes/ui_trace_chat.py +0 -0
  131. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/services/__init__.py +0 -0
  132. {sovara-0.2.1 → sovara-0.2.3}/src/sovara/server/services/trace_chat_service.py +0 -0
  133. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/dependency_links.txt +0 -0
  134. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/entry_points.txt +0 -0
  135. {sovara-0.2.1 → sovara-0.2.3}/src/sovara.egg-info/top_level.txt +0 -0
  136. {sovara-0.2.1 → sovara-0.2.3}/tests/test_graph_payloads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sovara
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: A development tool for LLM call visualization and debugging
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -33,6 +33,8 @@ Requires-Dist: langchain; extra == "dev"
33
33
  Requires-Dist: langchain-anthropic; extra == "dev"
34
34
  Requires-Dist: langchain-openai; extra == "dev"
35
35
  Requires-Dist: websocket-client; extra == "dev"
36
+ Provides-Extra: desktop
37
+ Requires-Dist: pyinstaller>=6.11; extra == "desktop"
36
38
  Provides-Extra: examples
37
39
  Requires-Dist: openai; extra == "examples"
38
40
  Requires-Dist: anthropic; extra == "examples"
@@ -5,7 +5,7 @@ requires = ["setuptools"]
5
5
  build-backend = "setuptools.build_meta"
6
6
 
7
7
  [tool.setuptools.package-data]
8
- "sovara.assets" = ["SKILL.md"]
8
+ "sovara.assets" = ["SKILL.md", "web_app/**/*"]
9
9
 
10
10
  [tool.setuptools]
11
11
  package-dir = {"" = "src"}
@@ -26,7 +26,7 @@ line-length = 100
26
26
 
27
27
  [project]
28
28
  name = "sovara"
29
- version = "0.2.1"
29
+ version = "0.2.3"
30
30
  description = "A development tool for LLM call visualization and debugging"
31
31
  readme = "docs/release/PYPI_DESC.md"
32
32
  requires-python = ">=3.10"
@@ -65,6 +65,10 @@ dev = [
65
65
  "websocket-client"
66
66
  ]
67
67
 
68
+ desktop = [
69
+ "pyinstaller>=6.11",
70
+ ]
71
+
68
72
  examples = [
69
73
  "openai",
70
74
  "anthropic",
@@ -208,6 +208,40 @@ This will produce the full result (no preview) of the specific keys that match t
208
208
  }
209
209
  ```
210
210
 
211
+ ### Inspect persisted logs
212
+ If you want to inspect the captured stdout/stderr transcript for a run, use the `logs` command. Unlike `probe` and `runs`, this command prints plain text rather than JSON.
213
+
214
+ The tool is generally structured like this.
215
+
216
+ ```
217
+ usage: so-cli logs [-h] [--tail TAIL] [--grep GREP] [--context CONTEXT] [--line-numbers] run_id
218
+
219
+ positional arguments:
220
+ run_id Run ID to fetch logs for
221
+
222
+ options:
223
+ -h, --help show this help message and exit
224
+ --tail TAIL Show only the last N displayed lines
225
+ --grep GREP Filter displayed lines using a regex pattern
226
+ --context CONTEXT Show N lines of context around each grep match
227
+ --line-numbers Prefix displayed lines with original line numbers
228
+ ```
229
+
230
+ `run_id` accepts a full UUID or any unambiguous prefix, just like `probe`.
231
+
232
+ Examples:
233
+ To inspect the last 40 lines of a run, run
234
+
235
+ ```bash
236
+ uv run so-cli logs b6aaf796 --tail 40
237
+ ```
238
+
239
+ To search a large log for cache misses with nearby context and original line numbers, run
240
+
241
+ ```bash
242
+ uv run so-cli logs b6aaf796 --grep "Cache miss" --context 2 --line-numbers
243
+ ```
244
+
211
245
  ### List and manage runs
212
246
  You can manage past runs that you did with the `runs` command, which is structured in the following way
213
247
 
@@ -3,7 +3,6 @@ import time
3
3
  import yaml
4
4
  from argparse import ArgumentParser, REMAINDER
5
5
  from sovara.common.constants import SOVARA_CONFIG
6
- from sovara.runner.agent_runner import AgentRunner
7
6
 
8
7
 
9
8
  def launch_command_parser():
@@ -32,6 +31,13 @@ def launch_command_parser():
32
31
  help="Change each process to interpret the launch script as a Python module, executing with the same behavior as 'python -m'.",
33
32
  )
34
33
 
34
+ parser.add_argument(
35
+ "-u",
36
+ "--unbuffered",
37
+ action="store_true",
38
+ help="Run with unbuffered stdout/stderr, like 'python -u'.",
39
+ )
40
+
35
41
  parser.add_argument(
36
42
  "script_path",
37
43
  type=str,
@@ -71,12 +77,15 @@ def _validate_launch_command(args):
71
77
 
72
78
 
73
79
  def launch_command(args):
80
+ from sovara.runner.agent_runner import AgentRunner
81
+
74
82
  args = _validate_launch_command(args)
75
83
 
76
84
  agent_runner = AgentRunner(
77
85
  script_path=args.script_path,
78
86
  script_args=args.script_args,
79
87
  is_module_execution=args.module,
88
+ unbuffered=args.unbuffered,
80
89
  run_name=args.run_name,
81
90
  )
82
91
  agent_runner.run()
@@ -12,9 +12,10 @@ if package_parent not in sys.path:
12
12
  import time
13
13
  import subprocess
14
14
  import json
15
- from argparse import ArgumentParser
15
+ from argparse import ArgumentParser, SUPPRESS
16
16
 
17
17
  from sovara.common.logger import logger, create_file_logger
18
+ from sovara.common.runtime import build_so_server_command
18
19
 
19
20
  from sovara.common.constants import (
20
21
  MAIN_SERVER_LOG,
@@ -22,6 +23,7 @@ from sovara.common.constants import (
22
23
  INFERENCE_SERVER_LOG,
23
24
  MAIN_SERVER_STARTUP_LOCK,
24
25
  HOST,
26
+ INFERENCE_PORT,
25
27
  PORT,
26
28
  SHUTDOWN_WAIT,
27
29
  )
@@ -127,7 +129,7 @@ def launch_daemon_server() -> bool:
127
129
  try:
128
130
  with open(MAIN_SERVER_LOG, "a+") as log_f:
129
131
  subprocess.Popen(
130
- [sys.executable, "-m", "sovara.cli.so_server", "_serve"],
132
+ build_so_server_command("_serve", "--host", HOST, "--port", str(PORT)),
131
133
  close_fds=True,
132
134
  start_new_session=True,
133
135
  stdin=subprocess.DEVNULL,
@@ -160,12 +162,61 @@ def server_command_parser():
160
162
  "clear-logs",
161
163
  "_clear_cache",
162
164
  "_serve",
165
+ "_serve-inference",
163
166
  ],
164
167
  help="The command to execute for the server.",
165
168
  )
169
+ parser.add_argument("--host", default=None, help=SUPPRESS)
170
+ parser.add_argument("--port", type=int, default=None, help=SUPPRESS)
166
171
  return parser
167
172
 
168
173
 
174
+ def _serve_main_server(host: str, port: int) -> None:
175
+ _server_logger.info(f"Imports completed in {_time.time() - _import_start:.2f}s")
176
+
177
+ # Save Python executable path to config for VS Code extension to use
178
+ from sovara.common.constants import SOVARA_CONFIG
179
+ from sovara.common.config import Config
180
+
181
+ try:
182
+ config = Config.from_yaml_file(SOVARA_CONFIG)
183
+ if config.python_executable != sys.executable:
184
+ config.python_executable = sys.executable
185
+ config.to_yaml_file(SOVARA_CONFIG)
186
+ _server_logger.info(f"Saved python_executable: {sys.executable}")
187
+ except Exception as e:
188
+ _server_logger.warning(f"Could not save python_executable: {e}")
189
+
190
+ import uvicorn
191
+ from sovara.server.app import create_app
192
+
193
+ _start = _time.time()
194
+ app = create_app()
195
+ _server_logger.info(f"FastAPI app created in {_time.time() - _start:.2f}s")
196
+
197
+ # Use Config+Server instead of uvicorn.run() so we can set
198
+ # server.should_exit = True for clean programmatic shutdown.
199
+ uv_config = uvicorn.Config(app, host=host, port=port, log_level="warning")
200
+ uv_server = uvicorn.Server(uv_config)
201
+
202
+ # Store reference on app so lifespan can wire it into ServerState.
203
+ app.state.uvicorn_server = uv_server
204
+ try:
205
+ uv_server.run()
206
+ finally:
207
+ try:
208
+ _stop_child_servers()
209
+ except Exception as exc:
210
+ _server_logger.warning(f"Could not stop child servers during shutdown: {exc}")
211
+ _release_startup_lock()
212
+
213
+
214
+ def _serve_inference_server(host: str, port: int) -> None:
215
+ from sovara.server.graph_analysis.inference_server import serve
216
+
217
+ serve(host=host, port=port)
218
+
219
+
169
220
  def execute_server_command(args):
170
221
  if args.command == "start":
171
222
  if _is_server_running():
@@ -250,43 +301,12 @@ def execute_server_command(args):
250
301
  return
251
302
 
252
303
  elif args.command == "_serve":
253
- _server_logger.info(f"Imports completed in {_time.time() - _import_start:.2f}s")
254
-
255
- # Save Python executable path to config for VS Code extension to use
256
- from sovara.common.constants import SOVARA_CONFIG
257
- from sovara.common.config import Config
258
-
259
- try:
260
- config = Config.from_yaml_file(SOVARA_CONFIG)
261
- if config.python_executable != sys.executable:
262
- config.python_executable = sys.executable
263
- config.to_yaml_file(SOVARA_CONFIG)
264
- _server_logger.info(f"Saved python_executable: {sys.executable}")
265
- except Exception as e:
266
- _server_logger.warning(f"Could not save python_executable: {e}")
267
-
268
- import uvicorn
269
- from sovara.server.app import create_app
270
-
271
- _start = _time.time()
272
- app = create_app()
273
- _server_logger.info(f"FastAPI app created in {_time.time() - _start:.2f}s")
274
-
275
- # Use Config+Server instead of uvicorn.run() so we can set
276
- # server.should_exit = True for clean programmatic shutdown.
277
- uv_config = uvicorn.Config(app, host=HOST, port=PORT, log_level="warning")
278
- uv_server = uvicorn.Server(uv_config)
304
+ _serve_main_server(args.host or HOST, args.port or PORT)
305
+ return
279
306
 
280
- # Store reference on app so lifespan can wire it into ServerState.
281
- app.state.uvicorn_server = uv_server
282
- try:
283
- uv_server.run()
284
- finally:
285
- try:
286
- _stop_child_servers()
287
- except Exception as exc:
288
- _server_logger.warning(f"Could not stop child servers during shutdown: {exc}")
289
- _release_startup_lock()
307
+ elif args.command == "_serve-inference":
308
+ _serve_inference_server(args.host or HOST, args.port or INFERENCE_PORT)
309
+ return
290
310
 
291
311
 
292
312
  def main():
@@ -2,6 +2,7 @@ import atexit
2
2
  import os
3
3
  import sys
4
4
  import json
5
+ import re
5
6
  import shlex
6
7
  import shutil
7
8
  import uuid
@@ -190,6 +191,77 @@ def _fetch_all_runs() -> list[dict]:
190
191
  return runs
191
192
 
192
193
 
194
+ def format_log_output(
195
+ log_text: str,
196
+ *,
197
+ tail: int | None = None,
198
+ grep: str | None = None,
199
+ context: int = 0,
200
+ line_numbers: bool = False,
201
+ ) -> str:
202
+ """Render persisted run logs with optional filtering and line-number formatting."""
203
+ if tail is not None and tail < 0:
204
+ raise ValueError("--tail must be greater than or equal to 0")
205
+ if context < 0:
206
+ raise ValueError("--context must be greater than or equal to 0")
207
+
208
+ if grep is None and tail is None and context == 0 and not line_numbers:
209
+ return log_text
210
+
211
+ lines = log_text.splitlines()
212
+ entries: list[tuple[int, str] | None]
213
+
214
+ if grep:
215
+ try:
216
+ pattern = re.compile(grep)
217
+ except re.error as exc:
218
+ raise ValueError(f"Invalid regex: {exc}") from exc
219
+
220
+ ranges: list[tuple[int, int]] = []
221
+ for index, line in enumerate(lines):
222
+ if not pattern.search(line):
223
+ continue
224
+
225
+ start = max(0, index - context)
226
+ end = min(len(lines) - 1, index + context)
227
+ if ranges and start <= ranges[-1][1] + 1:
228
+ ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
229
+ else:
230
+ ranges.append((start, end))
231
+
232
+ entries = []
233
+ for range_index, (start, end) in enumerate(ranges):
234
+ if range_index > 0:
235
+ entries.append(None)
236
+ for line_index in range(start, end + 1):
237
+ entries.append((line_index + 1, lines[line_index]))
238
+ else:
239
+ entries = [(index + 1, line) for index, line in enumerate(lines)]
240
+
241
+ if tail is not None:
242
+ entries = entries[-tail:] if tail > 0 else []
243
+
244
+ if not entries:
245
+ return ""
246
+
247
+ numbered_lines = [entry for entry in entries if entry is not None]
248
+ number_width = len(str(max((line_number for line_number, _line in numbered_lines), default=1)))
249
+
250
+ rendered_lines: list[str] = []
251
+ for entry in entries:
252
+ if entry is None:
253
+ rendered_lines.append("--")
254
+ continue
255
+
256
+ line_number, line = entry
257
+ if line_numbers:
258
+ rendered_lines.append(f"{line_number:>{number_width}}: {line}")
259
+ else:
260
+ rendered_lines.append(line)
261
+
262
+ return "\n".join(rendered_lines) + "\n"
263
+
264
+
193
265
  def _spawn_rerun(prepared: dict, timeout: float | None = None) -> dict:
194
266
  """
195
267
  Spawn a rerun process for a run and block until completion.
@@ -311,11 +383,16 @@ def record_command(args) -> None:
311
383
 
312
384
  atexit.register(cleanup_run_file)
313
385
 
314
- cmd = [sys.executable, "-m", "sovara.cli.so_record"]
386
+ cmd = [sys.executable]
387
+ if args.unbuffered:
388
+ cmd.append("-u")
389
+ cmd.extend(["-m", "sovara.cli.so_record"])
315
390
  if args.run_name:
316
391
  cmd.extend(["--run-name", args.run_name])
317
392
  if args.module:
318
393
  cmd.append("-m")
394
+ if args.unbuffered:
395
+ cmd.append("-u")
319
396
  cmd.append(args.script_path)
320
397
  cmd.extend(args.script_args)
321
398
 
@@ -382,6 +459,27 @@ def probe_command(args) -> None:
382
459
  output_json(result)
383
460
 
384
461
 
462
+ def logs_command(args) -> None:
463
+ """Show the persisted log for a run."""
464
+ result = _server_request("GET", f"/ui/run/{args.run_id}/log")
465
+ if result.get("status") == "error":
466
+ output_json(result)
467
+
468
+ try:
469
+ rendered = format_log_output(
470
+ result.get("log", ""),
471
+ tail=args.tail,
472
+ grep=args.grep,
473
+ context=args.context,
474
+ line_numbers=args.line_numbers,
475
+ )
476
+ except ValueError as exc:
477
+ output_json({"status": "error", "error": str(exc)})
478
+
479
+ sys.stdout.write(rendered)
480
+ sys.stdout.flush()
481
+
482
+
385
483
  def runs_command(args) -> None:
386
484
  """List runs via the REST API."""
387
485
  import re
@@ -965,7 +1063,7 @@ def create_parser() -> ArgumentParser:
965
1063
  """Create the argument parser with subcommands."""
966
1064
  parser = ArgumentParser(
967
1065
  prog="so-cli",
968
- description="CLI for programmatic interaction with Sovara dataflow system. All output is JSON.",
1066
+ description="CLI for programmatic interaction with Sovara dataflow system. Most commands output JSON; `logs` outputs plain text.",
969
1067
  )
970
1068
  subparsers = parser.add_subparsers(dest="command", required=True)
971
1069
 
@@ -980,6 +1078,11 @@ def create_parser() -> ArgumentParser:
980
1078
  action="store_true",
981
1079
  help="Run script_path as a Python module (like python -m)",
982
1080
  )
1081
+ record.add_argument(
1082
+ "-u", "--unbuffered",
1083
+ action="store_true",
1084
+ help="Run with unbuffered stdout/stderr, like python -u",
1085
+ )
983
1086
  record.add_argument("--run-name", help="Human-readable name for this run")
984
1087
  record.add_argument(
985
1088
  "--timeout",
@@ -1028,6 +1131,33 @@ def create_parser() -> ArgumentParser:
1028
1131
  help="Filter keys using regex pattern on flattened keys (e.g., 'messages.*content'). Lists use index notation: content.0.hello",
1029
1132
  )
1030
1133
 
1134
+ logs = subparsers.add_parser(
1135
+ "logs",
1136
+ help="Show a run's persisted logs",
1137
+ description="Show the persisted log transcript for a run with optional filtering.",
1138
+ )
1139
+ logs.add_argument("run_id", help="Run UUID or unambiguous prefix to fetch logs for")
1140
+ logs.add_argument(
1141
+ "--tail",
1142
+ type=int,
1143
+ help="Show only the last N displayed lines",
1144
+ )
1145
+ logs.add_argument(
1146
+ "--grep",
1147
+ help="Filter displayed lines using a regex pattern",
1148
+ )
1149
+ logs.add_argument(
1150
+ "--context",
1151
+ type=int,
1152
+ default=0,
1153
+ help="Show N lines of context around each grep match",
1154
+ )
1155
+ logs.add_argument(
1156
+ "--line-numbers",
1157
+ action="store_true",
1158
+ help="Prefix displayed lines with original line numbers",
1159
+ )
1160
+
1031
1161
  # runs subcommand
1032
1162
  runs = subparsers.add_parser(
1033
1163
  "runs",
@@ -1356,6 +1486,8 @@ def main():
1356
1486
  record_command(args)
1357
1487
  elif args.command == "probe":
1358
1488
  probe_command(args)
1489
+ elif args.command == "logs":
1490
+ logs_command(args)
1359
1491
  elif args.command == "runs":
1360
1492
  runs_command(args)
1361
1493
  elif args.command == "edit-and-rerun":
@@ -0,0 +1,15 @@
1
+ """Helpers for launching Sovara subprocesses across source and frozen builds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+
8
+ def is_frozen_runtime() -> bool:
9
+ return bool(getattr(sys, "frozen", False))
10
+
11
+
12
+ def build_so_server_command(*args: str) -> list[str]:
13
+ if is_frozen_runtime():
14
+ return [sys.executable, *args]
15
+ return [sys.executable, "-m", "sovara.cli.so_server", *args]
@@ -31,6 +31,7 @@ from sovara.runner.context_manager import (
31
31
  )
32
32
  from sovara.common.utils import set_server_url, http_post
33
33
  from sovara.runner.monkey_patching.apply_monkey_patches import apply_all_monkey_patches
34
+ from sovara.runner.run_log_capture import RunLogCapture
34
35
 
35
36
 
36
37
  def _log_error(context: str, exception: Exception) -> None:
@@ -128,12 +129,14 @@ class AgentRunner:
128
129
  script_path: str,
129
130
  script_args: List[str],
130
131
  is_module_execution: bool,
132
+ unbuffered: bool = False,
131
133
  sample_id: Optional[str] = None,
132
134
  run_name: Optional[str] = None,
133
135
  ):
134
136
  self.script_path = script_path
135
137
  self.script_args = script_args
136
138
  self.is_module_execution = is_module_execution
139
+ self.unbuffered = unbuffered
137
140
  self.sample_id = sample_id
138
141
  self.run_name = run_name
139
142
 
@@ -238,6 +241,7 @@ class AgentRunner:
238
241
  """Generate the appropriate command for restarting the script."""
239
242
  original_command = " ".join(shlex.quote(arg) for arg in sys.argv)
240
243
  python_executable = sys.executable
244
+ interpreter_flags = "-u " if self.unbuffered else ""
241
245
 
242
246
  if not self._is_debugpy_session():
243
247
  return f"{python_executable} {original_command}"
@@ -251,20 +255,36 @@ class AgentRunner:
251
255
  if "launcher" in cmdline_str and "--" in parent_cmdline:
252
256
  dash_index = parent_cmdline.index("--")
253
257
  original_args = " ".join(shlex.quote(arg) for arg in parent_cmdline[dash_index + 1:])
254
- return f"/usr/bin/env {python_executable} {original_args}"
258
+ return f"/usr/bin/env {python_executable} {interpreter_flags}{original_args}".strip()
255
259
 
256
260
  if "-m" in parent_cmdline and "debugpy" in parent_cmdline:
257
261
  if self.is_module_execution:
258
262
  target_args = f"-m {self.script_path} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
259
263
  else:
260
264
  target_args = f"{shlex.quote(self.script_path)} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
261
- return f"{python_executable} {target_args}"
265
+ return f"{python_executable} {interpreter_flags}{target_args}".strip()
262
266
 
263
267
  if self.is_module_execution:
264
268
  target_args = f"-m {self.script_path} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
265
269
  else:
266
270
  target_args = f"{shlex.quote(self.script_path)} {' '.join(shlex.quote(arg) for arg in self.script_args)}"
267
- return f"{python_executable} {target_args}"
271
+ return f"{python_executable} {interpreter_flags}{target_args}".strip()
272
+
273
+ def _apply_python_runtime_flags(self) -> None:
274
+ """Apply interpreter-like runtime flags that matter for in-process execution."""
275
+ if not self.unbuffered:
276
+ return
277
+
278
+ os.environ["PYTHONUNBUFFERED"] = "1"
279
+ for stream_name in ("stdout", "stderr"):
280
+ stream = getattr(sys, stream_name, None)
281
+ reconfigure = getattr(stream, "reconfigure", None)
282
+ if not callable(reconfigure):
283
+ continue
284
+ try:
285
+ reconfigure(line_buffering=True, write_through=True)
286
+ except ValueError:
287
+ logger.debug(f"Could not reconfigure sys.{stream_name} for unbuffered mode")
268
288
 
269
289
  def _register_with_server(self) -> None:
270
290
  """Register with the server via HTTP POST."""
@@ -357,6 +377,23 @@ class AgentRunner:
357
377
  traceback.print_exc()
358
378
  return 1
359
379
 
380
+ def _append_run_log(self, log_chunk: str) -> None:
381
+ if not self.run_id or not log_chunk:
382
+ return
383
+ http_post("/runner/update-log", {
384
+ "run_id": self.run_id,
385
+ "log_text": log_chunk,
386
+ "replace": False,
387
+ }, timeout=2.0)
388
+
389
+ def _run_user_code_with_output_capture(self) -> int:
390
+ if not self.run_id:
391
+ return self._execute_user_code()
392
+ with RunLogCapture(
393
+ append_callback=self._append_run_log,
394
+ ):
395
+ return self._execute_user_code()
396
+
360
397
  def _run_debug_mode(self) -> int:
361
398
  """Run in debug mode with persistent restart loop."""
362
399
  logger.info("[AgentRunner] Debug mode detected. Running with restart capability.")
@@ -368,7 +405,7 @@ class AgentRunner:
368
405
  self._apply_runtime_setup()
369
406
  first_run = False
370
407
 
371
- exit_code = self._execute_user_code()
408
+ exit_code = self._run_user_code_with_output_capture()
372
409
  logger.info(f"[AgentRunner] Script completed with exit code {exit_code}. Waiting...")
373
410
 
374
411
  while not self.shutdown_flag and not self.restart_event.is_set():
@@ -387,11 +424,12 @@ class AgentRunner:
387
424
  def _run_normal_mode(self) -> int:
388
425
  """Run in normal mode (single execution)."""
389
426
  self._apply_runtime_setup()
390
- return self._execute_user_code()
427
+ return self._run_user_code_with_output_capture()
391
428
 
392
429
  def run(self) -> None:
393
430
  """Main entry point."""
394
431
  try:
432
+ self._apply_python_runtime_flags()
395
433
  self._setup_environment()
396
434
  ensure_server_running()
397
435