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.
- symphony_code-0.0.1/LICENSE +21 -0
- symphony_code-0.0.1/PKG-INFO +66 -0
- symphony_code-0.0.1/README.md +41 -0
- symphony_code-0.0.1/pyproject.toml +52 -0
- symphony_code-0.0.1/pyproject.toml.orig +52 -0
- symphony_code-0.0.1/src/coding_agent/__init__.py +20 -0
- symphony_code-0.0.1/src/coding_agent/_lazy.py +17 -0
- symphony_code-0.0.1/src/coding_agent/addons/__init__.py +35 -0
- symphony_code-0.0.1/src/coding_agent/addons/approvals.py +224 -0
- symphony_code-0.0.1/src/coding_agent/addons/compaction/__init__.py +36 -0
- symphony_code-0.0.1/src/coding_agent/addons/compaction/addon.py +88 -0
- symphony_code-0.0.1/src/coding_agent/addons/compaction/compactor.py +138 -0
- symphony_code-0.0.1/src/coding_agent/addons/compaction/prompts.py +20 -0
- symphony_code-0.0.1/src/coding_agent/addons/compaction/transcript.py +118 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/__init__.py +68 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/addon.py +199 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/evaluators.py +245 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/handlers.py +66 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/policy.py +277 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/protocol.py +192 -0
- symphony_code-0.0.1/src/coding_agent/addons/evaluation/state.py +284 -0
- symphony_code-0.0.1/src/coding_agent/addons/langfuse/__init__.py +24 -0
- symphony_code-0.0.1/src/coding_agent/addons/langfuse/addon.py +465 -0
- symphony_code-0.0.1/src/coding_agent/addons/langfuse/install.py +79 -0
- symphony_code-0.0.1/src/coding_agent/addons/langfuse/serialize.py +158 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/__init__.py +33 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/addon.py +181 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/loop.py +247 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/prompts.py +26 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/sanitize.py +64 -0
- symphony_code-0.0.1/src/coding_agent/addons/learning/store.py +522 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/__init__.py +18 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/bridge.py +74 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/discovery.py +34 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/report.py +33 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/runtime.py +76 -0
- symphony_code-0.0.1/src/coding_agent/addons/manager/tool_changes.py +16 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/__init__.py +41 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/active.py +132 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/artifacts.py +110 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/collection.py +166 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/compatibility.py +30 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/images.py +133 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/jsonl.py +1195 -0
- symphony_code-0.0.1/src/coding_agent/addons/persistence/presentation.py +104 -0
- symphony_code-0.0.1/src/coding_agent/addons/plugins/__init__.py +19 -0
- symphony_code-0.0.1/src/coding_agent/addons/plugins/install.py +37 -0
- symphony_code-0.0.1/src/coding_agent/addons/plugins/manager.py +140 -0
- symphony_code-0.0.1/src/coding_agent/addons/plugins/models.py +50 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/__init__.py +28 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/addon.py +26 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/args.py +78 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/frontmatter.py +57 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/models.py +57 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/registry.py +109 -0
- symphony_code-0.0.1/src/coding_agent/addons/skills/tool.py +54 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/__init__.py +5 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/addon.py +106 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/credentials.py +42 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/playback.py +42 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/speak.py +287 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/stream.py +651 -0
- symphony_code-0.0.1/src/coding_agent/addons/voice/summary.py +126 -0
- symphony_code-0.0.1/src/coding_agent/agent.py +523 -0
- symphony_code-0.0.1/src/coding_agent/bench/__init__.py +1 -0
- symphony_code-0.0.1/src/coding_agent/bench/cli.py +200 -0
- symphony_code-0.0.1/src/coding_agent/bench/patch.py +78 -0
- symphony_code-0.0.1/src/coding_agent/commands/__init__.py +5 -0
- symphony_code-0.0.1/src/coding_agent/commands/catalog.py +179 -0
- symphony_code-0.0.1/src/coding_agent/commands/jev_argument.py +40 -0
- symphony_code-0.0.1/src/coding_agent/config/__init__.py +51 -0
- symphony_code-0.0.1/src/coding_agent/config/config.py +376 -0
- symphony_code-0.0.1/src/coding_agent/config/credentials.py +153 -0
- symphony_code-0.0.1/src/coding_agent/config/personalities.json +34 -0
- symphony_code-0.0.1/src/coding_agent/config/personalities.py +170 -0
- symphony_code-0.0.1/src/coding_agent/config/prompts.py +67 -0
- symphony_code-0.0.1/src/coding_agent/protocols/__init__.py +5 -0
- symphony_code-0.0.1/src/coding_agent/protocols/acp/README.md +36 -0
- symphony_code-0.0.1/src/coding_agent/protocols/acp/__init__.py +27 -0
- symphony_code-0.0.1/src/coding_agent/protocols/acp/mapping.py +164 -0
- symphony_code-0.0.1/src/coding_agent/protocols/acp/server.py +261 -0
- symphony_code-0.0.1/src/coding_agent/protocols/acp/transport.py +72 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/README.md +36 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/__init__.py +37 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/catalog.py +49 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/commands.py +218 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/server.py +181 -0
- symphony_code-0.0.1/src/coding_agent/protocols/stdio/transport.py +71 -0
- symphony_code-0.0.1/src/coding_agent/py.typed +0 -0
- symphony_code-0.0.1/src/coding_agent/resources/__init__.py +5 -0
- symphony_code-0.0.1/src/coding_agent/resources/usage.py +264 -0
- symphony_code-0.0.1/src/coding_agent/run/__init__.py +1 -0
- symphony_code-0.0.1/src/coding_agent/run/cli.py +99 -0
- symphony_code-0.0.1/src/coding_agent/run/detach.py +37 -0
- symphony_code-0.0.1/src/coding_agent/run/interrupted.py +67 -0
- symphony_code-0.0.1/src/coding_agent/run/log_sink.py +46 -0
- symphony_code-0.0.1/src/coding_agent/tools/__init__.py +73 -0
- symphony_code-0.0.1/src/coding_agent/tools/ask_user.py +61 -0
- symphony_code-0.0.1/src/coding_agent/tools/base.py +136 -0
- symphony_code-0.0.1/src/coding_agent/tools/bash.py +288 -0
- symphony_code-0.0.1/src/coding_agent/tools/bash_jobs.py +116 -0
- symphony_code-0.0.1/src/coding_agent/tools/generate_image.py +123 -0
- symphony_code-0.0.1/src/coding_agent/tools/memory.py +100 -0
- symphony_code-0.0.1/src/coding_agent/tools/patch.py +218 -0
- symphony_code-0.0.1/src/coding_agent/tools/read_file.py +167 -0
- symphony_code-0.0.1/src/coding_agent/tools/search.py +433 -0
- symphony_code-0.0.1/src/coding_agent/tools/web_search.py +151 -0
- symphony_code-0.0.1/src/coding_agent/tools/write_file.py +40 -0
- symphony_code-0.0.1/src/coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
- symphony_code-0.0.1/src/coding_agent/tui/__init__.py +22 -0
- symphony_code-0.0.1/src/coding_agent/tui/__main__.py +122 -0
- symphony_code-0.0.1/src/coding_agent/tui/app.py +456 -0
- symphony_code-0.0.1/src/coding_agent/tui/chrome/__init__.py +33 -0
- symphony_code-0.0.1/src/coding_agent/tui/chrome/footer.py +274 -0
- symphony_code-0.0.1/src/coding_agent/tui/chrome/topbar.py +259 -0
- symphony_code-0.0.1/src/coding_agent/tui/commands/__init__.py +45 -0
- symphony_code-0.0.1/src/coding_agent/tui/commands/catalog.py +35 -0
- symphony_code-0.0.1/src/coding_agent/tui/commands/manager.py +557 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/__init__.py +18 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/input.py +219 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/slash_menu.py +341 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/surface.py +330 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/voice.py +267 -0
- symphony_code-0.0.1/src/coding_agent/tui/composer/voice_mode.py +491 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/__init__.py +23 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/events.py +728 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/sink.py +115 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/state.py +145 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/subagent.py +531 -0
- symphony_code-0.0.1/src/coding_agent/tui/runtime/turn.py +206 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/__init__.py +54 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/ask.py +69 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/context.py +195 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/dashboard.py +91 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/diff.py +302 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/extensions.py +68 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/file_selector.py +167 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/history.py +458 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/langfuse.py +142 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/learning.py +113 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/modal.py +131 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/onboard.py +455 -0
- symphony_code-0.0.1/src/coding_agent/tui/screens/resume.py +163 -0
- symphony_code-0.0.1/src/coding_agent/tui/theme/__init__.py +49 -0
- symphony_code-0.0.1/src/coding_agent/tui/theme/load.py +325 -0
- symphony_code-0.0.1/src/coding_agent/tui/theme/style.py +95 -0
- symphony_code-0.0.1/src/coding_agent/tui/theme/theme.toml +1687 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/__init__.py +53 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/activity.py +184 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/calls.py +560 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/code_view.py +52 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/images.py +250 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/labels.py +135 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/modal.py +137 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/partial_args.py +19 -0
- symphony_code-0.0.1/src/coding_agent/tui/tools/snapshots.py +382 -0
- symphony_code-0.0.1/src/coding_agent/tui/transcript/__init__.py +36 -0
- symphony_code-0.0.1/src/coding_agent/tui/transcript/messages.py +408 -0
- symphony_code-0.0.1/src/coding_agent/tui/transcript/process.py +504 -0
- symphony_code-0.0.1/src/coding_agent/tui/transcript/surface.py +345 -0
- 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"]
|