symphony-code 0.0.1__py3-none-any.whl

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. coding_agent/__init__.py +20 -0
  2. coding_agent/_lazy.py +17 -0
  3. coding_agent/addons/__init__.py +35 -0
  4. coding_agent/addons/approvals.py +224 -0
  5. coding_agent/addons/compaction/__init__.py +36 -0
  6. coding_agent/addons/compaction/addon.py +88 -0
  7. coding_agent/addons/compaction/compactor.py +138 -0
  8. coding_agent/addons/compaction/prompts.py +20 -0
  9. coding_agent/addons/compaction/transcript.py +118 -0
  10. coding_agent/addons/evaluation/__init__.py +68 -0
  11. coding_agent/addons/evaluation/addon.py +199 -0
  12. coding_agent/addons/evaluation/evaluators.py +245 -0
  13. coding_agent/addons/evaluation/handlers.py +66 -0
  14. coding_agent/addons/evaluation/policy.py +277 -0
  15. coding_agent/addons/evaluation/protocol.py +192 -0
  16. coding_agent/addons/evaluation/state.py +284 -0
  17. coding_agent/addons/langfuse/__init__.py +24 -0
  18. coding_agent/addons/langfuse/addon.py +465 -0
  19. coding_agent/addons/langfuse/install.py +79 -0
  20. coding_agent/addons/langfuse/serialize.py +158 -0
  21. coding_agent/addons/learning/__init__.py +33 -0
  22. coding_agent/addons/learning/addon.py +181 -0
  23. coding_agent/addons/learning/loop.py +247 -0
  24. coding_agent/addons/learning/prompts.py +26 -0
  25. coding_agent/addons/learning/sanitize.py +64 -0
  26. coding_agent/addons/learning/store.py +522 -0
  27. coding_agent/addons/manager/__init__.py +18 -0
  28. coding_agent/addons/manager/bridge.py +74 -0
  29. coding_agent/addons/manager/discovery.py +34 -0
  30. coding_agent/addons/manager/report.py +33 -0
  31. coding_agent/addons/manager/runtime.py +76 -0
  32. coding_agent/addons/manager/tool_changes.py +16 -0
  33. coding_agent/addons/persistence/__init__.py +41 -0
  34. coding_agent/addons/persistence/active.py +132 -0
  35. coding_agent/addons/persistence/artifacts.py +110 -0
  36. coding_agent/addons/persistence/collection.py +166 -0
  37. coding_agent/addons/persistence/compatibility.py +30 -0
  38. coding_agent/addons/persistence/images.py +133 -0
  39. coding_agent/addons/persistence/jsonl.py +1195 -0
  40. coding_agent/addons/persistence/presentation.py +104 -0
  41. coding_agent/addons/plugins/__init__.py +19 -0
  42. coding_agent/addons/plugins/install.py +37 -0
  43. coding_agent/addons/plugins/manager.py +140 -0
  44. coding_agent/addons/plugins/models.py +50 -0
  45. coding_agent/addons/skills/__init__.py +28 -0
  46. coding_agent/addons/skills/addon.py +26 -0
  47. coding_agent/addons/skills/args.py +78 -0
  48. coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +77 -0
  49. coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +12 -0
  50. coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +16 -0
  51. coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +16 -0
  52. coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +13 -0
  53. coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +11 -0
  54. coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +18 -0
  55. coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +11 -0
  56. coding_agent/addons/skills/frontmatter.py +57 -0
  57. coding_agent/addons/skills/models.py +57 -0
  58. coding_agent/addons/skills/registry.py +109 -0
  59. coding_agent/addons/skills/tool.py +54 -0
  60. coding_agent/addons/voice/__init__.py +5 -0
  61. coding_agent/addons/voice/addon.py +106 -0
  62. coding_agent/addons/voice/credentials.py +42 -0
  63. coding_agent/addons/voice/playback.py +42 -0
  64. coding_agent/addons/voice/speak.py +287 -0
  65. coding_agent/addons/voice/stream.py +651 -0
  66. coding_agent/addons/voice/summary.py +126 -0
  67. coding_agent/agent.py +523 -0
  68. coding_agent/bench/__init__.py +1 -0
  69. coding_agent/bench/cli.py +200 -0
  70. coding_agent/bench/patch.py +78 -0
  71. coding_agent/commands/__init__.py +5 -0
  72. coding_agent/commands/catalog.py +179 -0
  73. coding_agent/commands/jev_argument.py +40 -0
  74. coding_agent/config/__init__.py +51 -0
  75. coding_agent/config/config.py +376 -0
  76. coding_agent/config/credentials.py +153 -0
  77. coding_agent/config/personalities.json +34 -0
  78. coding_agent/config/personalities.py +170 -0
  79. coding_agent/config/prompts.py +67 -0
  80. coding_agent/protocols/__init__.py +5 -0
  81. coding_agent/protocols/acp/README.md +36 -0
  82. coding_agent/protocols/acp/__init__.py +27 -0
  83. coding_agent/protocols/acp/mapping.py +164 -0
  84. coding_agent/protocols/acp/server.py +261 -0
  85. coding_agent/protocols/acp/transport.py +72 -0
  86. coding_agent/protocols/stdio/README.md +36 -0
  87. coding_agent/protocols/stdio/__init__.py +37 -0
  88. coding_agent/protocols/stdio/catalog.py +49 -0
  89. coding_agent/protocols/stdio/commands.py +218 -0
  90. coding_agent/protocols/stdio/server.py +181 -0
  91. coding_agent/protocols/stdio/transport.py +71 -0
  92. coding_agent/py.typed +0 -0
  93. coding_agent/resources/__init__.py +5 -0
  94. coding_agent/resources/usage.py +264 -0
  95. coding_agent/run/__init__.py +1 -0
  96. coding_agent/run/cli.py +99 -0
  97. coding_agent/run/detach.py +37 -0
  98. coding_agent/run/interrupted.py +67 -0
  99. coding_agent/run/log_sink.py +46 -0
  100. coding_agent/tools/__init__.py +73 -0
  101. coding_agent/tools/ask_user.py +61 -0
  102. coding_agent/tools/base.py +136 -0
  103. coding_agent/tools/bash.py +288 -0
  104. coding_agent/tools/bash_jobs.py +116 -0
  105. coding_agent/tools/generate_image.py +123 -0
  106. coding_agent/tools/memory.py +100 -0
  107. coding_agent/tools/patch.py +218 -0
  108. coding_agent/tools/read_file.py +167 -0
  109. coding_agent/tools/search.py +433 -0
  110. coding_agent/tools/web_search.py +151 -0
  111. coding_agent/tools/write_file.py +40 -0
  112. coding_agent/tui/EFFICIENCY_PLAN.md +25 -0
  113. coding_agent/tui/__init__.py +22 -0
  114. coding_agent/tui/__main__.py +122 -0
  115. coding_agent/tui/app.py +456 -0
  116. coding_agent/tui/chrome/__init__.py +33 -0
  117. coding_agent/tui/chrome/footer.py +274 -0
  118. coding_agent/tui/chrome/topbar.py +259 -0
  119. coding_agent/tui/commands/__init__.py +45 -0
  120. coding_agent/tui/commands/catalog.py +35 -0
  121. coding_agent/tui/commands/manager.py +557 -0
  122. coding_agent/tui/composer/__init__.py +18 -0
  123. coding_agent/tui/composer/input.py +219 -0
  124. coding_agent/tui/composer/slash_menu.py +341 -0
  125. coding_agent/tui/composer/surface.py +330 -0
  126. coding_agent/tui/composer/voice.py +267 -0
  127. coding_agent/tui/composer/voice_mode.py +491 -0
  128. coding_agent/tui/runtime/__init__.py +23 -0
  129. coding_agent/tui/runtime/events.py +728 -0
  130. coding_agent/tui/runtime/sink.py +115 -0
  131. coding_agent/tui/runtime/state.py +145 -0
  132. coding_agent/tui/runtime/subagent.py +531 -0
  133. coding_agent/tui/runtime/turn.py +206 -0
  134. coding_agent/tui/screens/__init__.py +54 -0
  135. coding_agent/tui/screens/ask.py +69 -0
  136. coding_agent/tui/screens/context.py +195 -0
  137. coding_agent/tui/screens/dashboard.py +91 -0
  138. coding_agent/tui/screens/diff.py +302 -0
  139. coding_agent/tui/screens/extensions.py +68 -0
  140. coding_agent/tui/screens/file_selector.py +167 -0
  141. coding_agent/tui/screens/history.py +458 -0
  142. coding_agent/tui/screens/langfuse.py +142 -0
  143. coding_agent/tui/screens/learning.py +113 -0
  144. coding_agent/tui/screens/modal.py +131 -0
  145. coding_agent/tui/screens/onboard.py +455 -0
  146. coding_agent/tui/screens/resume.py +163 -0
  147. coding_agent/tui/theme/__init__.py +49 -0
  148. coding_agent/tui/theme/load.py +325 -0
  149. coding_agent/tui/theme/style.py +95 -0
  150. coding_agent/tui/theme/theme.toml +1687 -0
  151. coding_agent/tui/tools/__init__.py +53 -0
  152. coding_agent/tui/tools/activity.py +184 -0
  153. coding_agent/tui/tools/calls.py +560 -0
  154. coding_agent/tui/tools/code_view.py +52 -0
  155. coding_agent/tui/tools/images.py +250 -0
  156. coding_agent/tui/tools/labels.py +135 -0
  157. coding_agent/tui/tools/modal.py +137 -0
  158. coding_agent/tui/tools/partial_args.py +19 -0
  159. coding_agent/tui/tools/snapshots.py +382 -0
  160. coding_agent/tui/transcript/__init__.py +36 -0
  161. coding_agent/tui/transcript/messages.py +408 -0
  162. coding_agent/tui/transcript/process.py +504 -0
  163. coding_agent/tui/transcript/surface.py +345 -0
  164. coding_agent/tui/transcript/thinking_markdown.py +314 -0
  165. symphony_code-0.0.1.dist-info/METADATA +66 -0
  166. symphony_code-0.0.1.dist-info/RECORD +169 -0
  167. symphony_code-0.0.1.dist-info/WHEEL +4 -0
  168. symphony_code-0.0.1.dist-info/entry_points.txt +5 -0
  169. symphony_code-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,287 @@
1
+ """Speak the model's own text as it streams, not a canned line at the end.
2
+
3
+ One websocket to ``wss://api.x.ai/v1/tts`` stays open for the voice turn.
4
+ Completed sentences are sent as they arrive, and the MP3 frames that come back
5
+ are handed to the player immediately. Waiting for ``after_run`` and then
6
+ building one file is what made the reply feel late.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import base64
13
+ import contextlib
14
+ import json
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import threading
19
+ from typing import Callable
20
+ from urllib.parse import urlencode
21
+
22
+ from coding_agent.addons.voice.credentials import GROK_TTS_VOICE, grok_voice_credentials
23
+ from coding_agent.addons.voice.playback import play_mp3
24
+
25
+ TTS_URL = "wss://api.x.ai/v1/tts"
26
+ SpeakListener = Callable[[bool], None]
27
+
28
+
29
+ class StreamingSpeaker:
30
+ """Background TTS socket. ``feed`` takes model text; ``finish`` ends it."""
31
+
32
+ def __init__(self, *, on_speaking: SpeakListener | None = None) -> None:
33
+ self._on_speaking = on_speaking
34
+ self._loop = asyncio.new_event_loop()
35
+ self._thread = threading.Thread(target=self._run, name="grok-tts", daemon=True)
36
+ # call_soon_threadsafe queues callbacks even before run_until_complete starts.
37
+ # One sender preserves delta/done ordering while the connection is pending.
38
+ self._commands: asyncio.Queue[str | None] = asyncio.Queue()
39
+ self._lock = threading.Lock()
40
+ self._closed = False
41
+ self._finished = False
42
+ self._sent = ""
43
+ self._pending = ""
44
+ self._error = ""
45
+ self._task: asyncio.Task[None] | None = None
46
+ self._thread.start()
47
+
48
+ def feed(self, text: str, *, force: bool = False) -> None:
49
+ """Queue newly speakable text. ``force`` sends the current clause now."""
50
+ with self._lock:
51
+ if self._closed or self._finished or not text:
52
+ return
53
+ self._feed(text, force=force)
54
+
55
+ def _feed(self, text: str, *, force: bool = False) -> None:
56
+ fresh = text[len(self._sent):] if text.startswith(self._sent) else text
57
+ self._pending += fresh
58
+ self._sent = text
59
+ speakable, rest = _split_ready(self._pending)
60
+ if force and not speakable and self._pending.strip():
61
+ speakable, rest = self._pending.strip(), ""
62
+ self._pending = rest
63
+ if speakable:
64
+ self._submit(speakable)
65
+
66
+ def finish(self, text: str = "") -> None:
67
+ """Speak whatever remains, then close the socket after playback."""
68
+ with self._lock:
69
+ if self._closed or self._finished:
70
+ return
71
+ if text:
72
+ self._feed(text)
73
+ if self._pending.strip():
74
+ self._submit(self._pending.strip())
75
+ self._pending = ""
76
+ self._finished = True
77
+ self._submit(None)
78
+
79
+ def close(self, *, timeout: float = 0.0) -> None:
80
+ """Cancel the session. Idempotent; does not block unless ``timeout``.
81
+
82
+ The UI thread calls this, so by default it only queues cancellation
83
+ and the worker cleans up the socket and player on its own. App
84
+ shutdown passes a ``timeout`` to wait for that, bounded in case the
85
+ provider is unresponsive. A listener on the worker never joins itself.
86
+ """
87
+ with self._lock:
88
+ if not self._closed:
89
+ self._closed = True
90
+ with contextlib.suppress(RuntimeError):
91
+ self._loop.call_soon_threadsafe(self._cancel)
92
+ if timeout > 0 and threading.current_thread() is not self._thread:
93
+ self._thread.join(timeout=timeout)
94
+
95
+ def _submit(self, text: str | None) -> None:
96
+ with contextlib.suppress(RuntimeError):
97
+ self._loop.call_soon_threadsafe(self._commands.put_nowait, text)
98
+
99
+ def _cancel(self) -> None:
100
+ if self._task is not None:
101
+ self._task.cancel()
102
+
103
+ def _run(self) -> None:
104
+ asyncio.set_event_loop(self._loop)
105
+ try:
106
+ self._task = self._loop.create_task(self._session())
107
+ if self._closed:
108
+ self._task.cancel()
109
+ with contextlib.suppress(asyncio.CancelledError):
110
+ self._loop.run_until_complete(self._task)
111
+ finally:
112
+ self._loop.close()
113
+
114
+ async def _session(self) -> None:
115
+ socket = None
116
+ player = None
117
+ tasks: list[asyncio.Task[None]] = []
118
+ try:
119
+ import websockets
120
+
121
+ token, headers = grok_voice_credentials()
122
+ query = urlencode({
123
+ "voice": GROK_TTS_VOICE,
124
+ "language": "en",
125
+ "codec": "mp3",
126
+ "optimize_streaming_latency": 1,
127
+ })
128
+ socket = await websockets.connect(
129
+ f"{TTS_URL}?{query}",
130
+ additional_headers={"Authorization": f"Bearer {token}", **headers},
131
+ max_size=8 * 1024 * 1024,
132
+ )
133
+ player = self._player = _Mp3Stream(on_speaking=self._mark)
134
+ tasks = [asyncio.create_task(self._listen(socket)), asyncio.create_task(self._send(socket))]
135
+ done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
136
+ for task in done:
137
+ task.result()
138
+ except ImportError:
139
+ self._error = "Voice playback needs the websockets package."
140
+ except Exception as exc:
141
+ self._error = f"Voice playback failed · {exc}"
142
+ finally:
143
+ for task in tasks:
144
+ task.cancel()
145
+ if player is not None:
146
+ with contextlib.suppress(Exception):
147
+ player.close()
148
+ await asyncio.gather(*tasks, return_exceptions=True)
149
+ if socket is not None:
150
+ with contextlib.suppress(Exception):
151
+ await socket.close()
152
+ with self._lock:
153
+ self._closed = True
154
+ # Startup failures and empty audio still need to release the voice UI.
155
+ self._mark(False)
156
+
157
+ async def _send(self, socket: object) -> None:
158
+ while True:
159
+ text = await self._commands.get()
160
+ event = {"type": "text.done"} if text is None else {"type": "text.delta", "delta": text + " "}
161
+ await socket.send(json.dumps(event)) # type: ignore[attr-defined]
162
+
163
+ async def _listen(self, socket: object) -> None:
164
+ player: _Mp3Stream = self._player
165
+ async for raw in socket: # type: ignore[attr-defined]
166
+ if isinstance(raw, bytes):
167
+ player.write(raw)
168
+ continue
169
+ try:
170
+ event = json.loads(raw)
171
+ except json.JSONDecodeError:
172
+ continue
173
+ kind = str(event.get("type") or "")
174
+ if kind == "audio.delta":
175
+ payload = event.get("delta") or event.get("audio") or ""
176
+ if isinstance(payload, str) and payload:
177
+ player.write(base64.b64decode(payload))
178
+ elif kind == "audio.done":
179
+ break
180
+ elif kind == "error":
181
+ self._error = str(event.get("message") or "voice playback failed")
182
+ break
183
+ # Always finish. An error or a missing streaming player leaves the MP3
184
+ # in the buffer, and finish() plays that file. Returning early was
185
+ # dropping it.
186
+ await asyncio.to_thread(player.finish)
187
+
188
+ def _mark(self, speaking: bool) -> None:
189
+ if self._on_speaking is not None:
190
+ self._on_speaking(speaking)
191
+
192
+
193
+ def _split_ready(pending: str) -> tuple[str, str]:
194
+ """Return text that ends on a sentence boundary, and the unfinished tail.
195
+
196
+ The boundary is the punctuation itself. Waiting for the space after it held
197
+ the whole sentence until the next word arrived.
198
+ """
199
+ cut = max(pending.rfind("."), pending.rfind("?"), pending.rfind("!"), pending.rfind("\n"))
200
+ if cut < 0:
201
+ return "", pending
202
+ return pending[: cut + 1].strip(), pending[cut + 1:]
203
+
204
+
205
+ class _Mp3Stream:
206
+ """Play MP3 bytes as they arrive. ffmpeg reads the stream; afplay cannot."""
207
+
208
+ def __init__(self, *, on_speaking: SpeakListener) -> None:
209
+ self._on_speaking = on_speaking
210
+ self._process: subprocess.Popen[bytes] | None = None
211
+ self._buffer = bytearray()
212
+
213
+ def write(self, mp3: bytes) -> None:
214
+ if not mp3:
215
+ return
216
+ if self._process is None:
217
+ command = _mp3_stream_command()
218
+ if command is None:
219
+ self._buffer.extend(mp3)
220
+ return
221
+ self._process = subprocess.Popen(
222
+ command,
223
+ stdin=subprocess.PIPE,
224
+ stdout=subprocess.DEVNULL,
225
+ stderr=subprocess.DEVNULL,
226
+ )
227
+ self._on_speaking(True)
228
+ assert self._process.stdin is not None
229
+ try:
230
+ self._process.stdin.write(mp3)
231
+ self._process.stdin.flush()
232
+ except (BrokenPipeError, OSError):
233
+ self.finish()
234
+
235
+ def finish(self) -> None:
236
+ process = self._process
237
+ if process is None:
238
+ # macOS has afplay and no ffplay or mpv. afplay cannot read an
239
+ # MP3 stream, so the bytes were buffered above and never played.
240
+ if self._buffer:
241
+ audio = bytes(self._buffer)
242
+ self._buffer.clear()
243
+ self._on_speaking(True)
244
+ try:
245
+ play_mp3(audio)
246
+ finally:
247
+ self._on_speaking(False)
248
+ return
249
+ try:
250
+ if process.stdin is not None:
251
+ with contextlib.suppress(OSError):
252
+ process.stdin.close()
253
+ try:
254
+ process.wait(timeout=30)
255
+ except subprocess.TimeoutExpired:
256
+ process.kill()
257
+ process.wait(timeout=1)
258
+ finally:
259
+ if self._process is process:
260
+ self._process = None
261
+ self._on_speaking(False)
262
+
263
+ def close(self) -> None:
264
+ process = self._process
265
+ self._process = None
266
+ self._buffer.clear()
267
+ try:
268
+ if process is not None:
269
+ with contextlib.suppress(OSError):
270
+ process.kill()
271
+ if process.stdin is not None:
272
+ with contextlib.suppress(OSError):
273
+ process.stdin.close()
274
+ with contextlib.suppress(subprocess.TimeoutExpired):
275
+ process.wait(timeout=1)
276
+ finally:
277
+ self._on_speaking(False)
278
+
279
+
280
+ def _mp3_stream_command() -> list[str] | None:
281
+ if shutil.which("ffplay"):
282
+ return ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-"]
283
+ if shutil.which("mpv"):
284
+ return ["mpv", "--no-video", "--really-quiet", "-"]
285
+ if sys.platform != "darwin":
286
+ return None
287
+ return None