symphony-code 0.0.1__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 (169) hide show
  1. symphony_code-0.0.1/LICENSE +21 -0
  2. symphony_code-0.0.1/PKG-INFO +66 -0
  3. symphony_code-0.0.1/README.md +41 -0
  4. symphony_code-0.0.1/pyproject.toml +52 -0
  5. symphony_code-0.0.1/pyproject.toml.orig +52 -0
  6. symphony_code-0.0.1/src/coding_agent/__init__.py +20 -0
  7. symphony_code-0.0.1/src/coding_agent/_lazy.py +17 -0
  8. symphony_code-0.0.1/src/coding_agent/addons/__init__.py +35 -0
  9. symphony_code-0.0.1/src/coding_agent/addons/approvals.py +224 -0
  10. symphony_code-0.0.1/src/coding_agent/addons/compaction/__init__.py +36 -0
  11. symphony_code-0.0.1/src/coding_agent/addons/compaction/addon.py +88 -0
  12. symphony_code-0.0.1/src/coding_agent/addons/compaction/compactor.py +138 -0
  13. symphony_code-0.0.1/src/coding_agent/addons/compaction/prompts.py +20 -0
  14. symphony_code-0.0.1/src/coding_agent/addons/compaction/transcript.py +118 -0
  15. symphony_code-0.0.1/src/coding_agent/addons/evaluation/__init__.py +68 -0
  16. symphony_code-0.0.1/src/coding_agent/addons/evaluation/addon.py +199 -0
  17. symphony_code-0.0.1/src/coding_agent/addons/evaluation/evaluators.py +245 -0
  18. symphony_code-0.0.1/src/coding_agent/addons/evaluation/handlers.py +66 -0
  19. symphony_code-0.0.1/src/coding_agent/addons/evaluation/policy.py +277 -0
  20. symphony_code-0.0.1/src/coding_agent/addons/evaluation/protocol.py +192 -0
  21. symphony_code-0.0.1/src/coding_agent/addons/evaluation/state.py +284 -0
  22. symphony_code-0.0.1/src/coding_agent/addons/langfuse/__init__.py +24 -0
  23. symphony_code-0.0.1/src/coding_agent/addons/langfuse/addon.py +465 -0
  24. symphony_code-0.0.1/src/coding_agent/addons/langfuse/install.py +79 -0
  25. symphony_code-0.0.1/src/coding_agent/addons/langfuse/serialize.py +158 -0
  26. symphony_code-0.0.1/src/coding_agent/addons/learning/__init__.py +33 -0
  27. symphony_code-0.0.1/src/coding_agent/addons/learning/addon.py +181 -0
  28. symphony_code-0.0.1/src/coding_agent/addons/learning/loop.py +247 -0
  29. symphony_code-0.0.1/src/coding_agent/addons/learning/prompts.py +26 -0
  30. symphony_code-0.0.1/src/coding_agent/addons/learning/sanitize.py +64 -0
  31. symphony_code-0.0.1/src/coding_agent/addons/learning/store.py +522 -0
  32. symphony_code-0.0.1/src/coding_agent/addons/manager/__init__.py +18 -0
  33. symphony_code-0.0.1/src/coding_agent/addons/manager/bridge.py +74 -0
  34. symphony_code-0.0.1/src/coding_agent/addons/manager/discovery.py +34 -0
  35. symphony_code-0.0.1/src/coding_agent/addons/manager/report.py +33 -0
  36. symphony_code-0.0.1/src/coding_agent/addons/manager/runtime.py +76 -0
  37. symphony_code-0.0.1/src/coding_agent/addons/manager/tool_changes.py +16 -0
  38. symphony_code-0.0.1/src/coding_agent/addons/persistence/__init__.py +41 -0
  39. symphony_code-0.0.1/src/coding_agent/addons/persistence/active.py +132 -0
  40. symphony_code-0.0.1/src/coding_agent/addons/persistence/artifacts.py +110 -0
  41. symphony_code-0.0.1/src/coding_agent/addons/persistence/collection.py +166 -0
  42. symphony_code-0.0.1/src/coding_agent/addons/persistence/compatibility.py +30 -0
  43. symphony_code-0.0.1/src/coding_agent/addons/persistence/images.py +133 -0
  44. symphony_code-0.0.1/src/coding_agent/addons/persistence/jsonl.py +1195 -0
  45. symphony_code-0.0.1/src/coding_agent/addons/persistence/presentation.py +104 -0
  46. symphony_code-0.0.1/src/coding_agent/addons/plugins/__init__.py +19 -0
  47. symphony_code-0.0.1/src/coding_agent/addons/plugins/install.py +37 -0
  48. symphony_code-0.0.1/src/coding_agent/addons/plugins/manager.py +140 -0
  49. symphony_code-0.0.1/src/coding_agent/addons/plugins/models.py +50 -0
  50. symphony_code-0.0.1/src/coding_agent/addons/skills/__init__.py +28 -0
  51. symphony_code-0.0.1/src/coding_agent/addons/skills/addon.py +26 -0
  52. symphony_code-0.0.1/src/coding_agent/addons/skills/args.py +78 -0
  53. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
  54. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
  55. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
  56. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
  57. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
  58. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
  59. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
  60. symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
  61. symphony_code-0.0.1/src/coding_agent/addons/skills/frontmatter.py +57 -0
  62. symphony_code-0.0.1/src/coding_agent/addons/skills/models.py +57 -0
  63. symphony_code-0.0.1/src/coding_agent/addons/skills/registry.py +109 -0
  64. symphony_code-0.0.1/src/coding_agent/addons/skills/tool.py +54 -0
  65. symphony_code-0.0.1/src/coding_agent/addons/voice/__init__.py +5 -0
  66. symphony_code-0.0.1/src/coding_agent/addons/voice/addon.py +106 -0
  67. symphony_code-0.0.1/src/coding_agent/addons/voice/credentials.py +42 -0
  68. symphony_code-0.0.1/src/coding_agent/addons/voice/playback.py +42 -0
  69. symphony_code-0.0.1/src/coding_agent/addons/voice/speak.py +287 -0
  70. symphony_code-0.0.1/src/coding_agent/addons/voice/stream.py +651 -0
  71. symphony_code-0.0.1/src/coding_agent/addons/voice/summary.py +126 -0
  72. symphony_code-0.0.1/src/coding_agent/agent.py +523 -0
  73. symphony_code-0.0.1/src/coding_agent/bench/__init__.py +1 -0
  74. symphony_code-0.0.1/src/coding_agent/bench/cli.py +200 -0
  75. symphony_code-0.0.1/src/coding_agent/bench/patch.py +78 -0
  76. symphony_code-0.0.1/src/coding_agent/commands/__init__.py +5 -0
  77. symphony_code-0.0.1/src/coding_agent/commands/catalog.py +179 -0
  78. symphony_code-0.0.1/src/coding_agent/commands/jev_argument.py +40 -0
  79. symphony_code-0.0.1/src/coding_agent/config/__init__.py +51 -0
  80. symphony_code-0.0.1/src/coding_agent/config/config.py +376 -0
  81. symphony_code-0.0.1/src/coding_agent/config/credentials.py +153 -0
  82. symphony_code-0.0.1/src/coding_agent/config/personalities.json +34 -0
  83. symphony_code-0.0.1/src/coding_agent/config/personalities.py +170 -0
  84. symphony_code-0.0.1/src/coding_agent/config/prompts.py +67 -0
  85. symphony_code-0.0.1/src/coding_agent/protocols/__init__.py +5 -0
  86. symphony_code-0.0.1/src/coding_agent/protocols/acp/README.md +36 -0
  87. symphony_code-0.0.1/src/coding_agent/protocols/acp/__init__.py +27 -0
  88. symphony_code-0.0.1/src/coding_agent/protocols/acp/mapping.py +164 -0
  89. symphony_code-0.0.1/src/coding_agent/protocols/acp/server.py +261 -0
  90. symphony_code-0.0.1/src/coding_agent/protocols/acp/transport.py +72 -0
  91. symphony_code-0.0.1/src/coding_agent/protocols/stdio/README.md +36 -0
  92. symphony_code-0.0.1/src/coding_agent/protocols/stdio/__init__.py +37 -0
  93. symphony_code-0.0.1/src/coding_agent/protocols/stdio/catalog.py +49 -0
  94. symphony_code-0.0.1/src/coding_agent/protocols/stdio/commands.py +218 -0
  95. symphony_code-0.0.1/src/coding_agent/protocols/stdio/server.py +181 -0
  96. symphony_code-0.0.1/src/coding_agent/protocols/stdio/transport.py +71 -0
  97. symphony_code-0.0.1/src/coding_agent/py.typed +0 -0
  98. symphony_code-0.0.1/src/coding_agent/resources/__init__.py +5 -0
  99. symphony_code-0.0.1/src/coding_agent/resources/usage.py +264 -0
  100. symphony_code-0.0.1/src/coding_agent/run/__init__.py +1 -0
  101. symphony_code-0.0.1/src/coding_agent/run/cli.py +99 -0
  102. symphony_code-0.0.1/src/coding_agent/run/detach.py +37 -0
  103. symphony_code-0.0.1/src/coding_agent/run/interrupted.py +67 -0
  104. symphony_code-0.0.1/src/coding_agent/run/log_sink.py +46 -0
  105. symphony_code-0.0.1/src/coding_agent/tools/__init__.py +73 -0
  106. symphony_code-0.0.1/src/coding_agent/tools/ask_user.py +61 -0
  107. symphony_code-0.0.1/src/coding_agent/tools/base.py +136 -0
  108. symphony_code-0.0.1/src/coding_agent/tools/bash.py +288 -0
  109. symphony_code-0.0.1/src/coding_agent/tools/bash_jobs.py +116 -0
  110. symphony_code-0.0.1/src/coding_agent/tools/generate_image.py +123 -0
  111. symphony_code-0.0.1/src/coding_agent/tools/memory.py +100 -0
  112. symphony_code-0.0.1/src/coding_agent/tools/patch.py +218 -0
  113. symphony_code-0.0.1/src/coding_agent/tools/read_file.py +167 -0
  114. symphony_code-0.0.1/src/coding_agent/tools/search.py +433 -0
  115. symphony_code-0.0.1/src/coding_agent/tools/web_search.py +151 -0
  116. symphony_code-0.0.1/src/coding_agent/tools/write_file.py +40 -0
  117. symphony_code-0.0.1/src/coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
  118. symphony_code-0.0.1/src/coding_agent/tui/__init__.py +22 -0
  119. symphony_code-0.0.1/src/coding_agent/tui/__main__.py +122 -0
  120. symphony_code-0.0.1/src/coding_agent/tui/app.py +456 -0
  121. symphony_code-0.0.1/src/coding_agent/tui/chrome/__init__.py +33 -0
  122. symphony_code-0.0.1/src/coding_agent/tui/chrome/footer.py +274 -0
  123. symphony_code-0.0.1/src/coding_agent/tui/chrome/topbar.py +259 -0
  124. symphony_code-0.0.1/src/coding_agent/tui/commands/__init__.py +45 -0
  125. symphony_code-0.0.1/src/coding_agent/tui/commands/catalog.py +35 -0
  126. symphony_code-0.0.1/src/coding_agent/tui/commands/manager.py +557 -0
  127. symphony_code-0.0.1/src/coding_agent/tui/composer/__init__.py +18 -0
  128. symphony_code-0.0.1/src/coding_agent/tui/composer/input.py +219 -0
  129. symphony_code-0.0.1/src/coding_agent/tui/composer/slash_menu.py +341 -0
  130. symphony_code-0.0.1/src/coding_agent/tui/composer/surface.py +330 -0
  131. symphony_code-0.0.1/src/coding_agent/tui/composer/voice.py +267 -0
  132. symphony_code-0.0.1/src/coding_agent/tui/composer/voice_mode.py +491 -0
  133. symphony_code-0.0.1/src/coding_agent/tui/runtime/__init__.py +23 -0
  134. symphony_code-0.0.1/src/coding_agent/tui/runtime/events.py +728 -0
  135. symphony_code-0.0.1/src/coding_agent/tui/runtime/sink.py +115 -0
  136. symphony_code-0.0.1/src/coding_agent/tui/runtime/state.py +145 -0
  137. symphony_code-0.0.1/src/coding_agent/tui/runtime/subagent.py +531 -0
  138. symphony_code-0.0.1/src/coding_agent/tui/runtime/turn.py +206 -0
  139. symphony_code-0.0.1/src/coding_agent/tui/screens/__init__.py +54 -0
  140. symphony_code-0.0.1/src/coding_agent/tui/screens/ask.py +69 -0
  141. symphony_code-0.0.1/src/coding_agent/tui/screens/context.py +195 -0
  142. symphony_code-0.0.1/src/coding_agent/tui/screens/dashboard.py +91 -0
  143. symphony_code-0.0.1/src/coding_agent/tui/screens/diff.py +302 -0
  144. symphony_code-0.0.1/src/coding_agent/tui/screens/extensions.py +68 -0
  145. symphony_code-0.0.1/src/coding_agent/tui/screens/file_selector.py +167 -0
  146. symphony_code-0.0.1/src/coding_agent/tui/screens/history.py +458 -0
  147. symphony_code-0.0.1/src/coding_agent/tui/screens/langfuse.py +142 -0
  148. symphony_code-0.0.1/src/coding_agent/tui/screens/learning.py +113 -0
  149. symphony_code-0.0.1/src/coding_agent/tui/screens/modal.py +131 -0
  150. symphony_code-0.0.1/src/coding_agent/tui/screens/onboard.py +455 -0
  151. symphony_code-0.0.1/src/coding_agent/tui/screens/resume.py +163 -0
  152. symphony_code-0.0.1/src/coding_agent/tui/theme/__init__.py +49 -0
  153. symphony_code-0.0.1/src/coding_agent/tui/theme/load.py +325 -0
  154. symphony_code-0.0.1/src/coding_agent/tui/theme/style.py +95 -0
  155. symphony_code-0.0.1/src/coding_agent/tui/theme/theme.toml +1687 -0
  156. symphony_code-0.0.1/src/coding_agent/tui/tools/__init__.py +53 -0
  157. symphony_code-0.0.1/src/coding_agent/tui/tools/activity.py +184 -0
  158. symphony_code-0.0.1/src/coding_agent/tui/tools/calls.py +560 -0
  159. symphony_code-0.0.1/src/coding_agent/tui/tools/code_view.py +52 -0
  160. symphony_code-0.0.1/src/coding_agent/tui/tools/images.py +250 -0
  161. symphony_code-0.0.1/src/coding_agent/tui/tools/labels.py +135 -0
  162. symphony_code-0.0.1/src/coding_agent/tui/tools/modal.py +137 -0
  163. symphony_code-0.0.1/src/coding_agent/tui/tools/partial_args.py +19 -0
  164. symphony_code-0.0.1/src/coding_agent/tui/tools/snapshots.py +382 -0
  165. symphony_code-0.0.1/src/coding_agent/tui/transcript/__init__.py +36 -0
  166. symphony_code-0.0.1/src/coding_agent/tui/transcript/messages.py +408 -0
  167. symphony_code-0.0.1/src/coding_agent/tui/transcript/process.py +504 -0
  168. symphony_code-0.0.1/src/coding_agent/tui/transcript/surface.py +345 -0
  169. symphony_code-0.0.1/src/coding_agent/tui/transcript/thinking_markdown.py +314 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarthak Chauhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: symphony-code
3
+ Version: 0.0.1
4
+ Summary: Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)
5
+ Author: Sarthak Chauhan
6
+ Author-email: Sarthak Chauhan <sarthakchauhann@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: symphony-core
10
+ Requires-Dist: symphony-harness
11
+ Requires-Dist: pillow>=12.3.0
12
+ Requires-Dist: pydantic>=2.0.0
13
+ Requires-Dist: pyyaml>=6.0.3
14
+ Requires-Dist: textual>=8.2.8
15
+ Requires-Dist: textual-image>=0.12.0
16
+ Requires-Dist: termaid>=0.9.0
17
+ Requires-Dist: sounddevice>=0.5.1
18
+ Requires-Dist: langfuse>=3.0.0 ; extra == 'langfuse'
19
+ Requires-Python: >=3.11
20
+ Project-URL: Homepage, https://github.com/Sarthakischauhan/symphony
21
+ Project-URL: Documentation, https://github.com/Sarthakischauhan/symphony/blob/main/docs/packages/symphony-code.md
22
+ Project-URL: Source, https://github.com/Sarthakischauhan/symphony/tree/main/coding_agent
23
+ Provides-Extra: langfuse
24
+ Description-Content-Type: text/markdown
25
+
26
+ # symphony-code
27
+
28
+ Run an AI coding agent in your terminal, or import the coding-agent API from the Symphony workspace. Package import name: `coding_agent`.
29
+
30
+ **Availability:** this package is not currently published on PyPI. To run it today, use a checkout of the repository:
31
+
32
+ ```sh
33
+ git clone https://github.com/Sarthakischauhan/symphony.git
34
+ cd symphony
35
+ uv sync
36
+ uv run --package symphony-code symphony --workspace /path/to/project
37
+ ```
38
+
39
+ `symphony`, `symphony-code`, and `coding-agent-tui` are aliases for the same terminal application. Configure a provider on first launch, or set credentials beforehand. See the [installation guide](../docs/getting-started/installation.md) and [quickstart](../docs/getting-started/quickstart.md).
40
+
41
+ ## Use the library
42
+
43
+ From the repository workspace:
44
+
45
+ ```python
46
+ from core_ai import build_default_registry, default_model_id
47
+ from coding_agent import CodingAgent
48
+
49
+ registry = build_default_registry()
50
+ agent = CodingAgent(
51
+ registry=registry,
52
+ model_id=default_model_id(registry),
53
+ workspace=".")
54
+ result = await agent.run("Fix the failing test")
55
+ print(result.output_text)
56
+ ```
57
+
58
+ ## Next steps
59
+
60
+ - [Terminal UI and controls](../docs/user-guide/tui.md)
61
+ - [Configuration and approvals](../docs/user-guide/configuration.md)
62
+ - [Tools and workspace access](../docs/user-guide/tools.md)
63
+ - [Sessions and resume](../docs/user-guide/sessions.md)
64
+ - [Package API guide](../docs/packages/symphony-code.md)
65
+
66
+ **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
@@ -0,0 +1,41 @@
1
+ # symphony-code
2
+
3
+ Run an AI coding agent in your terminal, or import the coding-agent API from the Symphony workspace. Package import name: `coding_agent`.
4
+
5
+ **Availability:** this package is not currently published on PyPI. To run it today, use a checkout of the repository:
6
+
7
+ ```sh
8
+ git clone https://github.com/Sarthakischauhan/symphony.git
9
+ cd symphony
10
+ uv sync
11
+ uv run --package symphony-code symphony --workspace /path/to/project
12
+ ```
13
+
14
+ `symphony`, `symphony-code`, and `coding-agent-tui` are aliases for the same terminal application. Configure a provider on first launch, or set credentials beforehand. See the [installation guide](../docs/getting-started/installation.md) and [quickstart](../docs/getting-started/quickstart.md).
15
+
16
+ ## Use the library
17
+
18
+ From the repository workspace:
19
+
20
+ ```python
21
+ from core_ai import build_default_registry, default_model_id
22
+ from coding_agent import CodingAgent
23
+
24
+ registry = build_default_registry()
25
+ agent = CodingAgent(
26
+ registry=registry,
27
+ model_id=default_model_id(registry),
28
+ workspace=".")
29
+ result = await agent.run("Fix the failing test")
30
+ print(result.output_text)
31
+ ```
32
+
33
+ ## Next steps
34
+
35
+ - [Terminal UI and controls](../docs/user-guide/tui.md)
36
+ - [Configuration and approvals](../docs/user-guide/configuration.md)
37
+ - [Tools and workspace access](../docs/user-guide/tools.md)
38
+ - [Sessions and resume](../docs/user-guide/sessions.md)
39
+ - [Package API guide](../docs/packages/symphony-code.md)
40
+
41
+ **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "symphony-code"
3
+ version = "0.0.1"
4
+ description = "Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "symphony-core",
11
+ "symphony-harness",
12
+ "pillow>=12.3.0",
13
+ "pydantic>=2.0.0",
14
+ "pyyaml>=6.0.3",
15
+ "textual>=8.2.8",
16
+ "textual-image>=0.12.0",
17
+ "termaid>=0.9.0",
18
+ "sounddevice>=0.5.1",
19
+ ]
20
+
21
+ [[project.authors]]
22
+ name = "Sarthak Chauhan"
23
+ email = "sarthakchauhann@gmail.com"
24
+
25
+ [project.optional-dependencies]
26
+ langfuse = ["langfuse>=3.0.0"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/Sarthakischauhan/symphony"
30
+ Documentation = "https://github.com/Sarthakischauhan/symphony/blob/main/docs/packages/symphony-code.md"
31
+ Source = "https://github.com/Sarthakischauhan/symphony/tree/main/coding_agent"
32
+
33
+ [project.scripts]
34
+ symphony = "coding_agent.tui.__main__:main"
35
+ symphony-code = "coding_agent.tui.__main__:main"
36
+ coding-agent-tui = "coding_agent.tui.__main__:main"
37
+
38
+ [tool.uv.sources.symphony-core]
39
+ workspace = true
40
+
41
+ [tool.uv.sources.symphony-harness]
42
+ workspace = true
43
+
44
+ [tool.uv.build-backend]
45
+ module-name = "coding_agent"
46
+
47
+ [dependency-groups]
48
+ dev = ["pytest>=9.1.1"]
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.9.8,<0.10.0"]
52
+ build-backend = "uv_build"
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "symphony-code"
3
+ version = "0.0.1"
4
+ description = "Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Sarthak Chauhan", email = "sarthakchauhann@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "symphony-core",
14
+ "symphony-harness",
15
+ "pillow>=12.3.0",
16
+ "pydantic>=2.0.0",
17
+ "pyyaml>=6.0.3",
18
+ "textual>=8.2.8",
19
+ "textual-image>=0.12.0",
20
+ "termaid>=0.9.0",
21
+ "sounddevice>=0.5.1",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ langfuse = ["langfuse>=3.0.0"]
26
+ [project.urls]
27
+ Homepage = "https://github.com/Sarthakischauhan/symphony"
28
+ Documentation = "https://github.com/Sarthakischauhan/symphony/blob/main/docs/packages/symphony-code.md"
29
+ Source = "https://github.com/Sarthakischauhan/symphony/tree/main/coding_agent"
30
+
31
+ [project.scripts]
32
+ # `symphony` is the primary command; the other two are aliases kept for
33
+ # compatibility and all resolve to the same entry point.
34
+ symphony = "coding_agent.tui.__main__:main"
35
+ symphony-code = "coding_agent.tui.__main__:main"
36
+ coding-agent-tui = "coding_agent.tui.__main__:main"
37
+
38
+ [tool.uv.sources]
39
+ symphony-core = { workspace = true }
40
+ symphony-harness = { workspace = true }
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=9.1.1",
45
+ ]
46
+
47
+ [build-system]
48
+ requires = ["uv_build>=0.9.8,<0.10.0"]
49
+ build-backend = "uv_build"
50
+
51
+ [tool.uv.build-backend]
52
+ module-name = "coding_agent"
@@ -0,0 +1,20 @@
1
+ from coding_agent.agent import CodingAgent, build_agent
2
+ from coding_agent.addons.learning import LearningAddon, LearningLoop, LearningReview, LearningStore, Lesson
3
+ from coding_agent.addons.persistence import JsonlPersistence, SessionSummary
4
+ from coding_agent.config.prompts import SUBAGENT_SYSTEM_PROMPT, SYSTEM_PROMPT
5
+ from coding_agent.tools import (
6
+ AskUserArgs, AskUserTool, BashArgs, BashTool,
7
+ GenerateImageArgs, GenerateImageTool, PatchArgs, PatchTool, ReadFileArgs, ReadFileTool,
8
+ SearchArgs, SearchTool, TOOL_CLASSES, ToolArgsModel, WebSearchArgs, WebSearchTool, WorkspaceTool,
9
+ WriteFileArgs, WriteFileTool, build_tools,
10
+ )
11
+
12
+ __all__ = [
13
+ "AskUserArgs", "AskUserTool", "BashArgs", "BashTool", "CodingAgent",
14
+ "GenerateImageArgs", "GenerateImageTool", "LearningAddon", "LearningLoop", "LearningReview",
15
+ "LearningStore", "Lesson", "PatchArgs", "PatchTool", "ReadFileArgs",
16
+ "ReadFileTool", "SUBAGENT_SYSTEM_PROMPT", "SYSTEM_PROMPT", "SearchArgs", "SearchTool",
17
+ "JsonlPersistence", "SessionSummary", "TOOL_CLASSES", "ToolArgsModel", "WebSearchArgs", "WebSearchTool",
18
+ "WorkspaceTool",
19
+ "WriteFileArgs", "WriteFileTool", "build_agent", "build_tools",
20
+ ]
@@ -0,0 +1,17 @@
1
+ """Defer an attribute until the module that owns it has finished importing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from typing import Any
7
+
8
+
9
+ def resolve(module_globals: dict[str, Any], name: str, exports: dict[str, tuple[str, str]]) -> Any:
10
+ target = exports.get(name)
11
+ if target is None:
12
+ module_name = module_globals.get("__name__", "module")
13
+ raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
14
+ source, attr = target
15
+ value = getattr(import_module(source), attr)
16
+ module_globals[name] = value
17
+ return value
@@ -0,0 +1,35 @@
1
+ """Product harness add-ons.
2
+
3
+ Feature packages keep each add-on's hooks and supporting machinery together.
4
+ This package exports the add-ons that ``CodingAgent`` mounts.
5
+ """
6
+
7
+ from coding_agent.addons.approvals import ApprovalAddon, ApprovalPolicy
8
+ from coding_agent.addons.compaction.addon import AiCompactionAddon, ai_compaction_from_config
9
+ from coding_agent.addons.evaluation.addon import JevAddon, jev_from_config
10
+ from coding_agent.addons.langfuse.addon import LangfuseAddon, langfuse_from_config
11
+ from coding_agent.addons.learning.addon import (
12
+ LEARNING_IDLE_DELAY_SECONDS,
13
+ LearningAddon,
14
+ SessionMemoryAddon,
15
+ inject_memory,
16
+ strip_memory_context,
17
+ )
18
+ from coding_agent.addons.skills.addon import SkillsAddon
19
+
20
+ __all__ = [
21
+ "AiCompactionAddon",
22
+ "ApprovalAddon",
23
+ "ApprovalPolicy",
24
+ "JevAddon",
25
+ "LEARNING_IDLE_DELAY_SECONDS",
26
+ "LangfuseAddon",
27
+ "LearningAddon",
28
+ "SessionMemoryAddon",
29
+ "SkillsAddon",
30
+ "ai_compaction_from_config",
31
+ "inject_memory",
32
+ "jev_from_config",
33
+ "langfuse_from_config",
34
+ "strip_memory_context",
35
+ ]
@@ -0,0 +1,224 @@
1
+ """Product approval rules, independent of the TUI.
2
+
3
+ The harness calls ``Addon.before_tool`` before running a tool. This add-on
4
+ implements symphony-code policy: durable deny/allow rules, which tools need
5
+ a prompt, how answers map to allow/deny, and that children do not inherit
6
+ the gate (except the deny rules in unattended runs). The TUI only renders the
7
+ question and returns the answer. Unattended runs never prompt: each decision a
8
+ human would have made is emitted as an ``auto_decision`` event instead.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from fnmatch import fnmatchcase
15
+ from pathlib import Path
16
+ from typing import Any, Dict, Optional, Sequence, Union
17
+
18
+ from core_harness import Addon, EventSink
19
+
20
+ from coding_agent.config import ApprovalConfig, ensure_spawn_settings
21
+
22
+ ALLOW_ONCE = "Allow once"
23
+ ALLOW_ALWAYS = "Always allow"
24
+ DENY = "Deny"
25
+ APPROVAL_CHOICES = (ALLOW_ONCE, ALLOW_ALWAYS, DENY)
26
+ AUTO_DECISION = "auto_decision"
27
+ RULE_SUBJECTS = {
28
+ "bash": "command", "write_file": "path", "patch": "path", "generate_image": "path",
29
+ "web_search": "query",
30
+ }
31
+
32
+
33
+ def rule_subjects(tool_name: str, arguments: Dict[str, Any], workspace: Path) -> set[str]:
34
+ """The bash command, ``web_search:<query>``, or a path normalized so ``../`` and symlinks cannot dodge a rule."""
35
+ field = RULE_SUBJECTS.get(tool_name)
36
+ raw = str(arguments.get(field) or "").strip() if field else ""
37
+ if not raw or field == "command":
38
+ return {raw} if raw else set()
39
+ if field == "query":
40
+ # Prefixed so a bash rule such as ``git *`` cannot allow a search.
41
+ return {f"{tool_name}:{raw}"}
42
+ joined = os.path.join(workspace, os.path.expanduser(raw))
43
+ absolute, resolved = os.path.normpath(joined), str(Path(joined).resolve())
44
+ return {absolute, resolved, os.path.relpath(absolute, workspace)}
45
+
46
+
47
+ def matching_rule(
48
+ patterns: Sequence[str], tool_name: str, arguments: Dict[str, Any], workspace: Path
49
+ ) -> Optional[str]:
50
+ """First pattern matching the bash command, the prefixed query, or any normalized form of the path."""
51
+ subjects = rule_subjects(tool_name, arguments, workspace)
52
+ return next((p for p in patterns if any(fnmatchcase(s, p) for s in subjects)), None)
53
+
54
+
55
+ class ApprovalPolicy:
56
+ """Path-aware rules. Callers pass the current ``ApprovalConfig``."""
57
+
58
+ def __init__(self, workspace: Union[str, Path]) -> None:
59
+ self.workspace = Path(workspace).expanduser().resolve()
60
+
61
+ def prompt_for(
62
+ self,
63
+ config: ApprovalConfig,
64
+ tool_name: str,
65
+ arguments: Dict[str, Any],
66
+ ) -> str:
67
+ """Return the question to ask, or ``""`` if the call is allowed.
68
+
69
+ Deny rules are not a prompt; ``ApprovalAddon`` checks them first.
70
+ """
71
+ if config.mode == "always_allow" or matching_rule(config.allow, tool_name, arguments, self.workspace):
72
+ return ""
73
+ if tool_name == "bash" and config.require_for_bash:
74
+ command = str(arguments.get("command") or "").strip()
75
+ return f"Allow bash command once?\n`{command}`"
76
+ if tool_name == "web_search" and config.require_for_web_search:
77
+ query = str(arguments.get("query") or "").strip()
78
+ return f"Allow web search once?\n`{query}`"
79
+ if tool_name in {"write_file", "generate_image"}:
80
+ path = str(arguments.get("path") or "").strip()
81
+ if config.require_for_overwrite and path and self._exists(path):
82
+ return f"Overwrite existing file `{path}`?"
83
+ return ""
84
+ if tool_name == "patch" and config.require_for_broad_patch:
85
+ path = str(arguments.get("path") or "").strip()
86
+ old = str(arguments.get("old_str") or arguments.get("old_string") or "")
87
+ replace_all = bool(arguments.get("replace_all"))
88
+ if replace_all or len(old) > config.broad_patch_chars:
89
+ kind = "global" if replace_all else "large"
90
+ return f"Apply a {kind} patch to `{path}`?"
91
+ return ""
92
+
93
+ def interpret(
94
+ self,
95
+ config: ApprovalConfig,
96
+ answer: str,
97
+ ) -> tuple[bool, bool]:
98
+ """Map a UI answer to ``(allowed, promote_to_always_allow)``."""
99
+ if config.mode == "always_allow":
100
+ return True, False
101
+ normalized = answer.strip().lower()
102
+ if normalized == ALLOW_ALWAYS.lower():
103
+ return True, True
104
+ return normalized in config.allow_answers, False
105
+
106
+ def _exists(self, path: str) -> bool:
107
+ try:
108
+ candidate = Path(path).expanduser()
109
+ target = (
110
+ candidate.resolve()
111
+ if candidate.is_absolute()
112
+ else (self.workspace / candidate).resolve()
113
+ )
114
+ return target.exists()
115
+ except OSError:
116
+ return False
117
+
118
+
119
+ class ApprovalAddon(Addon):
120
+ """Gate tools through deny rules, then :class:`ApprovalPolicy` prompts.
121
+
122
+ The live approval state is ``approvals`` on this add-on, the same for
123
+ every client. ``set_mode`` changes it and reports ``config_changed``.
124
+
125
+ Deny rules apply in every mode. ``fork_for_child`` returns ``None`` so
126
+ spawned children skip this gate, except in unattended runs where children
127
+ get a deny-only fork. That is product policy, documented in ``SECURITY.md``.
128
+ """
129
+
130
+ name = "approval"
131
+
132
+ def __init__(
133
+ self,
134
+ workspace: Union[str, Path],
135
+ plane: EventSink,
136
+ *,
137
+ approvals: Optional[ApprovalConfig] = None,
138
+ unattended: bool = False,
139
+ deny_only: bool = False,
140
+ ) -> None:
141
+ self.policy = ApprovalPolicy(workspace)
142
+ self.plane = plane
143
+ self.approvals = approvals
144
+ self.unattended = unattended
145
+ self.deny_only = deny_only
146
+
147
+ def fork_for_child(self, parent_harness: Any) -> Optional["ApprovalAddon"]:
148
+ del parent_harness
149
+ if not self.unattended:
150
+ return None
151
+ return ApprovalAddon(
152
+ self.policy.workspace, self.plane,
153
+ approvals=self.approvals, unattended=True, deny_only=True,
154
+ )
155
+
156
+ async def before_tool(
157
+ self,
158
+ *,
159
+ tool_name: str,
160
+ arguments: Dict[str, Any],
161
+ sink: Optional[EventSink] = None,
162
+ emit: Any = None,
163
+ tool_call: Any = None,
164
+ **_: Any,
165
+ ) -> Optional[str]:
166
+ approvals = self.approvals
167
+ if approvals is None:
168
+ return None
169
+ emit = emit or self.plane.emit
170
+ rule = matching_rule(approvals.deny, tool_name, arguments, self.policy.workspace)
171
+ if rule:
172
+ await emit(AUTO_DECISION, {"kind": "approval", "tool": tool_name, "decision": "deny", "rule": rule})
173
+ return f"tool call denied by rule {rule!r}"
174
+ if self.unattended and tool_name == "ask_user":
175
+ await emit(AUTO_DECISION, {"kind": "ask_user", "question": str(arguments.get("question") or "")})
176
+ if self.deny_only:
177
+ return None
178
+ prompt = self.policy.prompt_for(approvals, tool_name, arguments)
179
+ if not prompt:
180
+ return None
181
+ if self.unattended:
182
+ await emit(AUTO_DECISION, {"kind": "approval", "tool": tool_name, "decision": "allow", "rule": "unattended"})
183
+ return None
184
+ ask_on = sink or self.plane
185
+ answer = await ask_on.interaction.request_user_input(
186
+ question=prompt,
187
+ choices=APPROVAL_CHOICES,
188
+ default=ALLOW_ONCE,
189
+ kind="approval",
190
+ metadata={"tool_name": tool_name, "tool_call_id": str(getattr(tool_call, "id", "") or "")},
191
+ )
192
+ allowed, promote = self.policy.interpret(approvals, answer)
193
+ if promote:
194
+ await self.set_mode("always_allow", emit=emit)
195
+ if not allowed:
196
+ return "tool call denied by user"
197
+ return None
198
+
199
+ async def set_mode(self, mode: str, *, persist: bool = False, emit: Any = None) -> None:
200
+ """Change the approval mode for this agent and tell every client.
201
+
202
+ ``persist`` also writes it to the workspace settings.
203
+ """
204
+ current = self.approvals or ApprovalConfig()
205
+ self.approvals = ApprovalConfig.model_validate({**current.model_dump(), "mode": mode})
206
+ if persist:
207
+ config = ensure_spawn_settings(self.policy.workspace)
208
+ ensure_spawn_settings(
209
+ self.policy.workspace,
210
+ config=config.model_copy(update={"approvals": self.approvals}),
211
+ )
212
+ await (emit or self.plane.emit)("config_changed", {"approvals": {"mode": mode}})
213
+
214
+
215
+ __all__ = [
216
+ "ALLOW_ALWAYS",
217
+ "ALLOW_ONCE",
218
+ "APPROVAL_CHOICES",
219
+ "AUTO_DECISION",
220
+ "ApprovalAddon",
221
+ "ApprovalPolicy",
222
+ "DENY",
223
+ "matching_rule",
224
+ ]
@@ -0,0 +1,36 @@
1
+ """Model-backed conversation compaction for the coding agent.
2
+
3
+ The harness owns the ``Compactor`` protocol and the keep/drop rule
4
+ (``plan_keep_drop``). ``InferenceCompactor`` implements that protocol and
5
+ writes the compacted-context message with the active model. The add-on that
6
+ mounts it lives in ``coding_agent.addons.compaction.addon`` so this package does not
7
+ import the add-on layer while it is still loading.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from coding_agent._lazy import resolve
15
+ from coding_agent.addons.compaction.compactor import InferenceCompactor
16
+ from coding_agent.addons.compaction.transcript import (
17
+ build_compacted_message,
18
+ render_dropped_turns,
19
+ )
20
+
21
+ _ADDON_EXPORTS = {
22
+ "AiCompactionAddon": ("coding_agent.addons.compaction.addon", "AiCompactionAddon"),
23
+ "ai_compaction_from_config": ("coding_agent.addons.compaction.addon", "ai_compaction_from_config"),
24
+ }
25
+
26
+ __all__ = [
27
+ "AiCompactionAddon",
28
+ "InferenceCompactor",
29
+ "ai_compaction_from_config",
30
+ "build_compacted_message",
31
+ "render_dropped_turns",
32
+ ]
33
+
34
+
35
+ def __getattr__(name: str) -> Any:
36
+ return resolve(globals(), name, _ADDON_EXPORTS)
@@ -0,0 +1,88 @@
1
+ """Harness add-on that mounts ``InferenceCompactor``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from core_harness.addons import Addon
8
+ from core_harness.config import HarnessConfig
9
+
10
+ from coding_agent.addons.compaction.compactor import (
11
+ DEFAULT_SUMMARY_MAX_OUTPUT_TOKENS,
12
+ InferenceCompactor,
13
+ )
14
+ from coding_agent.addons.compaction.transcript import DEFAULT_TRANSCRIPT_MAX_CHARS
15
+ from coding_agent.config import CompactionConfig
16
+
17
+
18
+ class AiCompactionAddon(Addon):
19
+ """Mount an ``InferenceCompactor`` on ``harness.state.compactor``.
20
+
21
+ Compaction is a before-model hook: the harness runs the mounted compactor
22
+ from ``maybe_compact`` ahead of each model turn and from ``/compact`` on
23
+ demand, then fires ``on_compact`` for the other add-ons. This add-on only
24
+ decides *which* compactor is mounted. It binds the compactor to the
25
+ harness registry and its *current* ``model_id`` so the summary follows
26
+ ``/model`` switches.
27
+
28
+ It occupies the ``compaction`` add-on slot, so it replaces the harness
29
+ ``CompactionAddon`` rather than sitting beside it. Children fork a fresh
30
+ add-on with the same settings; the child then binds to its own registry
31
+ and model on attach.
32
+ """
33
+
34
+ name = "compaction"
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ keep_recent_tools: int = 32,
40
+ target_tokens: Optional[int] = None,
41
+ max_output_tokens: int = DEFAULT_SUMMARY_MAX_OUTPUT_TOKENS,
42
+ max_transcript_chars: int = DEFAULT_TRANSCRIPT_MAX_CHARS,
43
+ model_id: Optional[str] = None,
44
+ ) -> None:
45
+ self.keep_recent_tools = keep_recent_tools
46
+ self.target_tokens = target_tokens
47
+ self.max_output_tokens = max_output_tokens
48
+ self.max_transcript_chars = max_transcript_chars
49
+ self.model_id = model_id
50
+ self.compactor: Optional[InferenceCompactor] = None
51
+
52
+ def attach(self, harness: Any) -> None:
53
+ self.compactor = InferenceCompactor(
54
+ registry=harness.registry,
55
+ model_id=self.model_id or (lambda: str(harness.model_id)),
56
+ keep_recent_tools=self.keep_recent_tools,
57
+ target_tokens=self.target_tokens,
58
+ max_output_tokens=self.max_output_tokens,
59
+ max_transcript_chars=self.max_transcript_chars,
60
+ )
61
+ harness.state.compactor = self.compactor
62
+
63
+ def fork_for_child(self, parent_harness: Any) -> AiCompactionAddon:
64
+ """Fresh add-on with the same settings; binds to the child on attach."""
65
+ del parent_harness
66
+ return AiCompactionAddon(
67
+ keep_recent_tools=self.keep_recent_tools,
68
+ target_tokens=self.target_tokens,
69
+ max_output_tokens=self.max_output_tokens,
70
+ max_transcript_chars=self.max_transcript_chars,
71
+ model_id=self.model_id,
72
+ )
73
+
74
+
75
+ def ai_compaction_from_config(
76
+ harness_config: HarnessConfig,
77
+ compaction: CompactionConfig,
78
+ ) -> AiCompactionAddon:
79
+ """Build the add-on from harness keep/drop limits plus summary settings."""
80
+ return AiCompactionAddon(
81
+ keep_recent_tools=harness_config.compaction_keep_recent_tools,
82
+ target_tokens=harness_config.context_target_tokens,
83
+ max_output_tokens=compaction.max_output_tokens,
84
+ max_transcript_chars=compaction.max_transcript_chars,
85
+ )
86
+
87
+
88
+ __all__ = ["AiCompactionAddon", "ai_compaction_from_config"]