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