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,200 @@
1
+ """Headless ``symphony bench``: run ``CodingAgent``, write patch + result.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import sys
10
+ import tempfile
11
+ import tomllib
12
+ from pathlib import Path
13
+ from typing import Any, Sequence
14
+
15
+ from core_ai import has_configured_provider
16
+ from core_ai.providers.catalog import MissingProviderCredentials
17
+ from core_harness import EventSink, HarnessCancelled, HarnessLimitExceeded
18
+
19
+ from coding_agent.agent import CodingAgent
20
+ from coding_agent.bench.patch import git_head, write_workspace_patch
21
+ from coding_agent.config import (
22
+ ApprovalConfig,
23
+ CodingAgentConfig,
24
+ EvaluationConfig,
25
+ LearningConfig,
26
+ PluginsConfig,
27
+ default_coding_agent_harness,
28
+ )
29
+ from coding_agent.config.credentials import load_provider_env
30
+
31
+ PERSONALITIES = ("direct", "precise")
32
+
33
+
34
+ def _config_paths() -> list[Path]:
35
+ paths: list[Path] = []
36
+ env = os.environ.get("SYMPHONY_BENCH_CONFIG", "").strip()
37
+ if env:
38
+ paths.append(Path(env))
39
+ paths.append(Path("/opt/symphony/bench/config.toml"))
40
+ here = Path(__file__).resolve()
41
+ if len(here.parents) >= 5:
42
+ paths.append(here.parents[4] / "bench" / "config.toml")
43
+ return paths
44
+
45
+
46
+ def load_bench_config() -> dict[str, Any]:
47
+ for path in _config_paths():
48
+ if path.is_file():
49
+ payload = tomllib.loads(path.read_text(encoding="utf-8"))
50
+ if isinstance(payload, dict):
51
+ return payload
52
+ return {}
53
+
54
+
55
+ def build_parser(defaults: dict[str, Any] | None = None) -> argparse.ArgumentParser:
56
+ defaults = defaults if defaults is not None else load_bench_config()
57
+ testbed = str(defaults.get("testbed") or "/testbed")
58
+ workspace = testbed if Path(testbed).is_dir() else "."
59
+ parser = argparse.ArgumentParser(
60
+ prog="symphony bench",
61
+ description="Headless Symphony bench run. Writes workspace.patch and result.json.",
62
+ )
63
+ parser.add_argument("--workspace", default=workspace, help="Working directory (default: /testbed if present, else .)")
64
+ parser.add_argument(
65
+ "--instruction",
66
+ default="-",
67
+ help="Instruction file, or - for stdin",
68
+ )
69
+ parser.add_argument("--model", default=defaults.get("model"), help="provider:model id")
70
+ parser.add_argument(
71
+ "--max-turns",
72
+ type=int,
73
+ default=int(defaults["max_turns"]) if defaults.get("max_turns") is not None else None,
74
+ )
75
+ parser.add_argument(
76
+ "--timeout",
77
+ type=float,
78
+ default=float(defaults["timeout_sec"]) if defaults.get("timeout_sec") is not None else None,
79
+ help="Run time cap in seconds (harness max_runtime_seconds)",
80
+ )
81
+ parser.add_argument(
82
+ "--personality",
83
+ choices=PERSONALITIES,
84
+ default=str(defaults.get("personality") or "direct"),
85
+ help="Fixed personality (no picker)",
86
+ )
87
+ parser.add_argument(
88
+ "--jev",
89
+ dest="enable_jev",
90
+ action="store_true",
91
+ default=False,
92
+ help="Enable Jev critic mode (off by default)",
93
+ )
94
+ return parser
95
+
96
+
97
+ def read_instruction(value: str) -> str:
98
+ if value == "-":
99
+ if sys.stdin.isatty():
100
+ raise SystemExit("instruction required: pass --instruction FILE or pipe stdin")
101
+ return sys.stdin.read()
102
+ path = Path(value).expanduser()
103
+ if not path.is_file():
104
+ raise SystemExit(f"instruction file not found: {value}")
105
+ return path.read_text(encoding="utf-8")
106
+
107
+
108
+ def _write_result(workspace: Path, payload: dict[str, Any]) -> None:
109
+ (workspace / "result.json").write_text(
110
+ json.dumps(payload, indent=2) + "\n",
111
+ encoding="utf-8",
112
+ )
113
+
114
+
115
+ def _turns(sink: EventSink) -> int:
116
+ return sum(1 for event in sink.events if event.event_type == "turn_started")
117
+
118
+
119
+ def _isolate_home() -> None:
120
+ os.environ["HOME"] = tempfile.mkdtemp(prefix="symphony-bench-home-")
121
+
122
+
123
+ def _bench_config(
124
+ *,
125
+ personality: str,
126
+ enable_jev: bool,
127
+ max_turns: int | None,
128
+ timeout: float | None,
129
+ ) -> CodingAgentConfig:
130
+ harness = default_coding_agent_harness()
131
+ updates: dict[str, Any] = {}
132
+ if max_turns is not None:
133
+ updates["max_turns"] = max_turns
134
+ if timeout is not None:
135
+ updates["max_runtime_seconds"] = float(timeout)
136
+ if updates:
137
+ harness = harness.model_copy(update=updates)
138
+ return CodingAgentConfig(
139
+ harness=harness,
140
+ personality=personality,
141
+ learning=LearningConfig(enabled=False),
142
+ evaluation=EvaluationConfig(enabled=enable_jev),
143
+ plugins=PluginsConfig(enabled=False),
144
+ approvals=ApprovalConfig(mode="always_allow"),
145
+ )
146
+
147
+
148
+ async def _run_agent(agent: CodingAgent, instruction: str) -> None:
149
+ await agent.run(instruction)
150
+
151
+
152
+ def run_bench(args: argparse.Namespace) -> int:
153
+ workspace = Path(args.workspace).expanduser().resolve()
154
+ workspace.mkdir(parents=True, exist_ok=True)
155
+ instruction = read_instruction(args.instruction)
156
+ load_provider_env(workspace)
157
+ start_ref = git_head(workspace)
158
+ sink = EventSink()
159
+ ok = False
160
+ error: str | None = None
161
+ try:
162
+ if not has_configured_provider():
163
+ raise MissingProviderCredentials()
164
+ _isolate_home()
165
+ from core_ai import build_default_registry, default_model_id
166
+
167
+ registry = build_default_registry()
168
+ agent = CodingAgent(
169
+ registry=registry,
170
+ model_id=default_model_id(registry, args.model),
171
+ workspace=workspace,
172
+ config=_bench_config(
173
+ personality=args.personality,
174
+ enable_jev=bool(args.enable_jev),
175
+ max_turns=args.max_turns,
176
+ timeout=args.timeout,
177
+ ),
178
+ sink=sink,
179
+ )
180
+ asyncio.run(_run_agent(agent, instruction))
181
+ ok = True
182
+ except HarnessLimitExceeded:
183
+ ok = True
184
+ except HarnessCancelled as exc:
185
+ error = str(exc) or "cancelled"
186
+ except Exception as exc:
187
+ error = str(exc) or exc.__class__.__name__
188
+ turns = _turns(sink)
189
+ write_workspace_patch(workspace, start_ref=start_ref)
190
+ payload: dict[str, Any] = {"ok": ok, "turns": turns}
191
+ if error:
192
+ payload["error"] = error
193
+ _write_result(workspace, payload)
194
+ return 0 if ok else 1
195
+
196
+
197
+ def main(argv: Sequence[str] | None = None) -> int:
198
+ parser = build_parser()
199
+ args = parser.parse_args(list(argv) if argv is not None else None)
200
+ return run_bench(args)
@@ -0,0 +1,78 @@
1
+ """Git diff of the workspace from a recorded start ref."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ _EXCLUDE = ("workspace.patch", "result.json", "instruction.md", ".symphony")
11
+
12
+
13
+ def git_head(workspace: Path) -> str | None:
14
+ result = subprocess.run(
15
+ ["git", "rev-parse", "HEAD"],
16
+ cwd=workspace,
17
+ capture_output=True,
18
+ text=True,
19
+ check=False,
20
+ )
21
+ if result.returncode != 0:
22
+ return None
23
+ ref = result.stdout.strip()
24
+ return ref or None
25
+
26
+
27
+ def workspace_diff(workspace: Path, start_ref: str | None = None) -> str:
28
+ """Unified diff of tracked and untracked files versus ``start_ref`` (default HEAD)."""
29
+ workspace = workspace.resolve()
30
+ ref = start_ref or git_head(workspace)
31
+ if ref is None:
32
+ return ""
33
+ env = os.environ.copy()
34
+ with tempfile.TemporaryDirectory(prefix="symphony-bench-index-") as tmp:
35
+ env["GIT_INDEX_FILE"] = str(Path(tmp) / "index")
36
+ read = subprocess.run(
37
+ ["git", "read-tree", ref],
38
+ cwd=workspace,
39
+ env=env,
40
+ capture_output=True,
41
+ text=True,
42
+ check=False,
43
+ )
44
+ if read.returncode != 0:
45
+ return ""
46
+ subprocess.run(
47
+ ["git", "add", "-A", "--", "."],
48
+ cwd=workspace,
49
+ env=env,
50
+ capture_output=True,
51
+ check=False,
52
+ )
53
+ subprocess.run(
54
+ ["git", "reset", "-q", "--", *_EXCLUDE],
55
+ cwd=workspace,
56
+ env=env,
57
+ capture_output=True,
58
+ check=False,
59
+ )
60
+ diff = subprocess.run(
61
+ ["git", "diff", "--cached", "--binary", "--no-color"],
62
+ cwd=workspace,
63
+ env=env,
64
+ capture_output=True,
65
+ check=False,
66
+ )
67
+ return diff.stdout.decode("utf-8", errors="replace")
68
+
69
+
70
+ def write_workspace_patch(
71
+ workspace: Path,
72
+ dest: Path | None = None,
73
+ *,
74
+ start_ref: str | None = None,
75
+ ) -> Path:
76
+ dest = dest or workspace.resolve() / "workspace.patch"
77
+ dest.write_text(workspace_diff(workspace, start_ref), encoding="utf-8")
78
+ return dest
@@ -0,0 +1,5 @@
1
+ """Slash-command catalog and decisions shared by the TUI and the stdio host.
2
+
3
+ Pure data and matching. Nothing here imports Textual, so headless surfaces
4
+ (``protocols/stdio``) can read the same catalog the TUI shows.
5
+ """
@@ -0,0 +1,179 @@
1
+ """Slash-command catalogs and option matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable, Optional
7
+
8
+ from core_ai import list_models
9
+ from coding_agent.config.personalities import load_personalities
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SlashCommand:
14
+ name: str
15
+ description: str
16
+ argument: str = ""
17
+
18
+ @property
19
+ def usage(self) -> str:
20
+ return f"/{self.name}{f' {self.argument}' if self.argument else ''}"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ModelOption:
25
+ id: str
26
+ label: str
27
+ description: str
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class EffortOption:
32
+ id: str
33
+ label: str
34
+ description: str
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class PersonalityOption:
39
+ id: str
40
+ label: str
41
+ description: str
42
+
43
+
44
+ def model_options(providers: Iterable[str] | None = None) -> tuple[ModelOption, ...]:
45
+ """Build TUI choices from the core_ai catalog, optionally by provider."""
46
+ source = list_models() if providers is None else tuple(
47
+ model for provider in providers for model in list_models(provider)
48
+ )
49
+ return tuple(
50
+ ModelOption(
51
+ id=model.full_id,
52
+ label=model.id,
53
+ description=f"{model.provider} · {model.api.replace('_', ' ')}",
54
+ )
55
+ for model in source
56
+ )
57
+
58
+
59
+ def find_model(
60
+ value: str,
61
+ models: Iterable[ModelOption] | None = None,
62
+ ) -> Optional[ModelOption]:
63
+ models = model_options() if models is None else models
64
+ needle = value.strip().lower()
65
+ if not needle:
66
+ return None
67
+ matches = [
68
+ model for model in models
69
+ if needle in {model.id.lower(), model.id.split(":", 1)[-1].lower(), model.label.lower()}
70
+ ]
71
+ return matches[0] if len(matches) == 1 else None
72
+
73
+
74
+ def model_matches(
75
+ value: str,
76
+ models: Iterable[ModelOption] | None = None,
77
+ ) -> tuple[ModelOption, ...]:
78
+ models = model_options() if models is None else models
79
+ needle = value.strip().lower()
80
+ return tuple(model for model in models if not needle or needle in model.id.lower() or needle in model.label.lower())
81
+
82
+
83
+ EFFORT_CATALOG = (
84
+ EffortOption("default", "Default", "Use Symphony's balanced default"),
85
+ EffortOption("none", "None", "Fastest · no deliberate reasoning"),
86
+ EffortOption("low", "Low", "Fast · lightweight reasoning"),
87
+ EffortOption("medium", "Medium", "Balanced speed and depth"),
88
+ EffortOption("high", "High", "Deeper reasoning"),
89
+ EffortOption("xhigh", "Extra high", "Extended reasoning"),
90
+ EffortOption("max", "Maximum", "Deepest reasoning · GPT-5.6"),
91
+ )
92
+ EFFORTS = tuple(option.id for option in EFFORT_CATALOG)
93
+
94
+
95
+ SLASH_COMMANDS = (
96
+ SlashCommand("model", "View or switch the active model", "[model]"),
97
+ SlashCommand("personality", "View or switch the agent personality", "[id]"),
98
+ SlashCommand("voice", "Talk until you say done or press Ctrl+A again", ""),
99
+ SlashCommand("effort", "Set model reasoning effort", "[level]"),
100
+ SlashCommand("provider", "Sign in or add a model provider", "[name]"),
101
+ SlashCommand("langfuse", "Configure Langfuse telemetry and install the SDK", ""),
102
+ SlashCommand("jev", "Set a Jev finish rule or toggle it", "[on|off|rule]"),
103
+ SlashCommand("new", "Start a fresh conversation"),
104
+ SlashCommand("addon", "Create or improve a Symphony user addon", "[idea]"),
105
+ SlashCommand("reload", "Reload configuration from .env and user addons"),
106
+ SlashCommand("addons", "List loaded and failed user addons"),
107
+ SlashCommand("compact", "Keep recent messages and compact saved context"),
108
+ SlashCommand("status", "Show session, model, and context details"),
109
+ SlashCommand("dashboard", "Show live agents and their CPU, memory, and disk"),
110
+ SlashCommand("context", "Inspect active model context"),
111
+ SlashCommand("session", "Inspect session images, compactions, memory, and recorded events"),
112
+ SlashCommand("learning", "Browse curated memory topics and pending captures"),
113
+ SlashCommand("installed", "View installed skills and plugins"),
114
+ SlashCommand("diff", "Open the current workspace diff in a modal"),
115
+ SlashCommand("clear", "Clear the visible transcript"),
116
+ SlashCommand("help", "Show available slash commands"),
117
+ SlashCommand("quit", "Exit Symphony"),
118
+ )
119
+
120
+
121
+ def command_matches(
122
+ value: str,
123
+ *,
124
+ include_effort: bool = True,
125
+ extra: Iterable[SlashCommand] = (),
126
+ ) -> tuple[SlashCommand, ...]:
127
+ """Match built-in commands, then ``extra`` (user-addon commands)."""
128
+ if not value.startswith("/") or " " in value:
129
+ return ()
130
+ prefix = value[1:].lower()
131
+ return tuple(
132
+ command
133
+ for command in (*SLASH_COMMANDS, *extra)
134
+ if command.name.startswith(prefix)
135
+ and (include_effort or command.name != "effort")
136
+ )
137
+
138
+
139
+ def personality_options() -> tuple[PersonalityOption, ...]:
140
+ return tuple(
141
+ PersonalityOption(
142
+ item.id,
143
+ item.name,
144
+ item.description or item.system_addon.split(".", 1)[0],
145
+ )
146
+ for item in load_personalities()
147
+ )
148
+
149
+
150
+ def find_personality(
151
+ value: str,
152
+ personalities: Iterable[PersonalityOption] | None = None,
153
+ ) -> Optional[PersonalityOption]:
154
+ options = personality_options() if personalities is None else personalities
155
+ needle = value.strip().lower()
156
+ if not needle:
157
+ return None
158
+ matches = [
159
+ item
160
+ for item in options
161
+ if needle in {item.id.lower(), item.label.lower(), item.id.replace("_", " ").lower()}
162
+ ]
163
+ return matches[0] if len(matches) == 1 else None
164
+
165
+
166
+ def personality_matches(
167
+ value: str,
168
+ personalities: Iterable[PersonalityOption] | None = None,
169
+ ) -> tuple[PersonalityOption, ...]:
170
+ options = personality_options() if personalities is None else personalities
171
+ needle = value.strip().lower()
172
+ return tuple(
173
+ item
174
+ for item in options
175
+ if not needle
176
+ or needle in item.id.lower()
177
+ or needle in item.label.lower()
178
+ or needle in item.id.replace("_", " ").lower()
179
+ )
@@ -0,0 +1,40 @@
1
+ """Read the ``/jev`` argument once for every surface.
2
+
3
+ The TUI keeps a rule for the current session and the stdio protocol writes it
4
+ to config. Where the rule is stored stays with each surface; this module only
5
+ decides what the argument means.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+
13
+ _TOGGLE = frozenset({"", "toggle"})
14
+ _ON = frozenset({"on", "enable", "true", "1"})
15
+ _OFF = frozenset({"off", "disable", "false", "0"})
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class JevChange:
20
+ """Result of ``/jev <argument>``. ``rule`` is None when the rule is unchanged."""
21
+
22
+ enabled: bool
23
+ rule: Optional[str] = None
24
+
25
+
26
+ def parse_jev_argument(current: bool, argument: str) -> JevChange:
27
+ """Resolve ``/jev [toggle|on|off|<rule>]``.
28
+
29
+ Keywords are case-insensitive. Any other text is a rule (kept as typed,
30
+ minus surrounding whitespace) and turns Jev on.
31
+ """
32
+ text = argument.strip()
33
+ needle = text.lower()
34
+ if needle in _TOGGLE:
35
+ return JevChange(enabled=not current)
36
+ if needle in _ON:
37
+ return JevChange(enabled=True)
38
+ if needle in _OFF:
39
+ return JevChange(enabled=False)
40
+ return JevChange(enabled=True, rule=text)
@@ -0,0 +1,51 @@
1
+ """Configuration, credentials, personalities, and prompts for coding_agent."""
2
+
3
+ from coding_agent.config.config import (
4
+ ApprovalConfig,
5
+ BashConfig,
6
+ CodingAgentConfig,
7
+ CompactionConfig,
8
+ EvaluationConfig,
9
+ LEARNING_MIN_OUTPUT_TOKENS,
10
+ LangfuseConfig,
11
+ LearningConfig,
12
+ PluginsConfig,
13
+ ReadFileConfig,
14
+ SPAWN_SETTINGS_NAME,
15
+ SearchConfig,
16
+ SkillsConfig,
17
+ SettingsSource,
18
+ ToolsConfig,
19
+ WebSearchConfig,
20
+ default_coding_agent_harness,
21
+ ensure_spawn_settings,
22
+ load_coding_agent_config,
23
+ resolve_coding_agent_config,
24
+ spawn_settings_path,
25
+ symphony_dir,
26
+ )
27
+
28
+ __all__ = [
29
+ "ApprovalConfig",
30
+ "BashConfig",
31
+ "CodingAgentConfig",
32
+ "CompactionConfig",
33
+ "EvaluationConfig",
34
+ "LangfuseConfig",
35
+ "LEARNING_MIN_OUTPUT_TOKENS",
36
+ "LearningConfig",
37
+ "PluginsConfig",
38
+ "ReadFileConfig",
39
+ "SPAWN_SETTINGS_NAME",
40
+ "SearchConfig",
41
+ "SkillsConfig",
42
+ "SettingsSource",
43
+ "ToolsConfig",
44
+ "WebSearchConfig",
45
+ "default_coding_agent_harness",
46
+ "ensure_spawn_settings",
47
+ "load_coding_agent_config",
48
+ "resolve_coding_agent_config",
49
+ "spawn_settings_path",
50
+ "symphony_dir",
51
+ ]