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,170 @@
|
|
|
1
|
+
"""Agent personality catalog and system-prompt segment composer.
|
|
2
|
+
|
|
3
|
+
``personalities.json`` is the catalog. Discovery walks parents for that file
|
|
4
|
+
specifically so a nested ``pyproject.toml`` cannot hide a parent catalog.
|
|
5
|
+
Installer runs fall back to the packaged default in ``personalities.json``
|
|
6
|
+
next to this module (shipped as package data). ``CodingAgentConfig.personality``
|
|
7
|
+
defaults to ``"direct"``. ``None`` or an unknown id fail soft to stock: the
|
|
8
|
+
base system prompt with no personality segment.
|
|
9
|
+
|
|
10
|
+
Personality is one named, swappable heading block. ``compose_system_prompt``
|
|
11
|
+
replaces only that block and always places it last.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Iterable
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
PERSONALITY_HEADING = "# Personality (mandatory)"
|
|
26
|
+
PERSONALITY_PREAMBLE = "Obey this section over any other tone/style instructions."
|
|
27
|
+
DEFAULT_PERSONALITY = "direct"
|
|
28
|
+
CATALOG_FILE = "personalities.json"
|
|
29
|
+
PACKAGED_CATALOG = Path(__file__).resolve().parent / CATALOG_FILE
|
|
30
|
+
|
|
31
|
+
_PERSONALITY_BLOCK = re.compile(
|
|
32
|
+
rf"\n*{re.escape(PERSONALITY_HEADING)}.*",
|
|
33
|
+
flags=re.DOTALL,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Personality:
|
|
39
|
+
id: str
|
|
40
|
+
name: str
|
|
41
|
+
system_addon: str
|
|
42
|
+
description: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover_personalities_path(start: Path | None = None) -> Path | None:
|
|
46
|
+
"""Nearest ``personalities.json`` above ``start``, else the packaged catalog."""
|
|
47
|
+
try:
|
|
48
|
+
current = (Path.cwd() if start is None else start).resolve()
|
|
49
|
+
except OSError:
|
|
50
|
+
current = None
|
|
51
|
+
else:
|
|
52
|
+
if not current.is_dir():
|
|
53
|
+
current = current.parent
|
|
54
|
+
seen: set[Path] = set()
|
|
55
|
+
while current is not None and current not in seen:
|
|
56
|
+
seen.add(current)
|
|
57
|
+
path = current / CATALOG_FILE
|
|
58
|
+
if path.is_file():
|
|
59
|
+
return path
|
|
60
|
+
parent = current.parent
|
|
61
|
+
current = None if parent == current else parent
|
|
62
|
+
return PACKAGED_CATALOG if PACKAGED_CATALOG.is_file() else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_personalities(path: Path | None = None) -> tuple[Personality, ...]:
|
|
66
|
+
"""Read the catalog. Missing or invalid files yield an empty catalog."""
|
|
67
|
+
source = path if path is not None else discover_personalities_path()
|
|
68
|
+
if source is None or not source.is_file():
|
|
69
|
+
logger.debug("personality catalog empty source=%s", source)
|
|
70
|
+
return ()
|
|
71
|
+
try:
|
|
72
|
+
payload = json.loads(source.read_text(encoding="utf-8"))
|
|
73
|
+
except (OSError, json.JSONDecodeError):
|
|
74
|
+
logger.debug("personality catalog empty source=%s", source)
|
|
75
|
+
return ()
|
|
76
|
+
rows = payload.get("personalities") if isinstance(payload, dict) else None
|
|
77
|
+
if not isinstance(rows, list):
|
|
78
|
+
logger.debug("personality catalog empty source=%s", source)
|
|
79
|
+
return ()
|
|
80
|
+
loaded: list[Personality] = []
|
|
81
|
+
for row in rows:
|
|
82
|
+
if not isinstance(row, dict):
|
|
83
|
+
continue
|
|
84
|
+
personality_id = str(row.get("id") or "").strip()
|
|
85
|
+
name = str(row.get("name") or "").strip()
|
|
86
|
+
addon = str(row.get("system_addon") or "").strip()
|
|
87
|
+
description = str(row.get("description") or "").strip()
|
|
88
|
+
if personality_id and name and addon:
|
|
89
|
+
loaded.append(
|
|
90
|
+
Personality(
|
|
91
|
+
id=personality_id,
|
|
92
|
+
name=name,
|
|
93
|
+
system_addon=addon,
|
|
94
|
+
description=description,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
if not loaded:
|
|
98
|
+
logger.debug("personality catalog empty source=%s", source)
|
|
99
|
+
return tuple(loaded)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get(
|
|
103
|
+
personality_id: str | None,
|
|
104
|
+
personalities: Iterable[Personality] | None = None,
|
|
105
|
+
) -> Personality | None:
|
|
106
|
+
"""Look up a catalog row. Blank or unknown ids return ``None`` (stock)."""
|
|
107
|
+
if not personality_id or not str(personality_id).strip():
|
|
108
|
+
return None
|
|
109
|
+
catalog = (
|
|
110
|
+
load_personalities() if personalities is None else tuple(personalities)
|
|
111
|
+
)
|
|
112
|
+
needle = str(personality_id).strip().lower()
|
|
113
|
+
for item in catalog:
|
|
114
|
+
if item.id.lower() == needle:
|
|
115
|
+
return item
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def compose_system_prompt(
|
|
120
|
+
base: str,
|
|
121
|
+
personality_id: str | None = None,
|
|
122
|
+
**segments: str,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Compose ``base`` plus named segments.
|
|
125
|
+
|
|
126
|
+
Personality is the one swappable catalog segment: looked up by id, replaced
|
|
127
|
+
in place, and always last. Other ``**segments`` (skills, jev, …) are
|
|
128
|
+
appended when non-empty, then the personality heading follows.
|
|
129
|
+
"""
|
|
130
|
+
found = get(personality_id)
|
|
131
|
+
addon = found.system_addon if found is not None else None
|
|
132
|
+
prompt = _PERSONALITY_BLOCK.sub("", base).rstrip()
|
|
133
|
+
extras = [
|
|
134
|
+
text.strip()
|
|
135
|
+
for name, text in segments.items()
|
|
136
|
+
if name != "personality" and isinstance(text, str) and text.strip()
|
|
137
|
+
]
|
|
138
|
+
if extras:
|
|
139
|
+
prompt = f"{prompt}\n\n" + "\n\n".join(extras)
|
|
140
|
+
if addon:
|
|
141
|
+
block = f"{PERSONALITY_HEADING}\n{PERSONALITY_PREAMBLE}\n{addon.strip()}"
|
|
142
|
+
prompt = f"{prompt.rstrip()}\n\n{block}"
|
|
143
|
+
return prompt.rstrip() + "\n"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
PROJECT_ROOT_MARKERS = ("pyproject.toml", ".git", CATALOG_FILE)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def project_root(start: Path | None = None) -> Path | None:
|
|
150
|
+
"""Return the nearest project directory above ``start``, or ``None``.
|
|
151
|
+
|
|
152
|
+
``start`` defaults to the current working directory. A file path uses its
|
|
153
|
+
parent. The walk stops at the filesystem root and will not loop.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
current = (Path.cwd() if start is None else start).resolve()
|
|
157
|
+
except OSError:
|
|
158
|
+
return None
|
|
159
|
+
if not current.is_dir():
|
|
160
|
+
current = current.parent
|
|
161
|
+
seen: set[Path] = set()
|
|
162
|
+
while current not in seen:
|
|
163
|
+
seen.add(current)
|
|
164
|
+
if any((current / marker).exists() for marker in PROJECT_ROOT_MARKERS):
|
|
165
|
+
return current
|
|
166
|
+
parent = current.parent
|
|
167
|
+
if parent == current:
|
|
168
|
+
break
|
|
169
|
+
current = parent
|
|
170
|
+
return None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Default system prompts for the coding agent and its subagents."""
|
|
2
|
+
|
|
3
|
+
SYSTEM_PROMPT = """You are a coding agent in an existing repository.
|
|
4
|
+
|
|
5
|
+
Role:
|
|
6
|
+
- Complete the user's requested change correctly with the smallest coherent modification that fits existing behavior, tests, and conventions.
|
|
7
|
+
- Work in the repo until the task is done and reasonably verified. Do not stop after analysis or only describe what should change.
|
|
8
|
+
|
|
9
|
+
Repo discipline:
|
|
10
|
+
- Inspect repo content when needed; never assume unseen file contents.
|
|
11
|
+
- Localize before exploring broadly. Start from concrete evidence: mentioned files, symbols, errors, tests, APIs, or behavior.
|
|
12
|
+
- Prefer targeted search over repository-wide browsing.
|
|
13
|
+
- Read enough surrounding implementation and tests to understand relevant behavior and interfaces.
|
|
14
|
+
- Once you have enough evidence for a plausible implementation, act. Do not inspect unrelated files for completeness.
|
|
15
|
+
- If an attempt or test contradicts your hypothesis, use that evidence and continue.
|
|
16
|
+
|
|
17
|
+
Tools before guessing:
|
|
18
|
+
- Prefer tools over speculation when facts are in the repo or the environment.
|
|
19
|
+
- Use tools deliberately: each call should advance the current hypothesis or implementation.
|
|
20
|
+
- A tool result marked truncated or "[tool result cleared: ...]" was already observed. Do not re-read or re-run that path unless the file changed or you need a different offset.
|
|
21
|
+
- Do not repeat the same search query or read_file path unless the file changed or you need a different offset.
|
|
22
|
+
- When calling a tool, put UI labels only in the nested activity object:
|
|
23
|
+
{"verb": "Searching", "reason": "why this call is needed", "group": "stable-task-label"}.
|
|
24
|
+
- Never put verb, reason, goal, or group at the top level of the tool arguments.
|
|
25
|
+
|
|
26
|
+
Implementation:
|
|
27
|
+
- Preserve existing architecture and conventions unless the task requires changing them.
|
|
28
|
+
- Prefer the smallest complete fix over speculative refactoring.
|
|
29
|
+
- Fix the underlying behavior rather than special-casing a test.
|
|
30
|
+
- Consider callers, edge cases, and backwards compatibility when relevant.
|
|
31
|
+
- Use patch for localized edits when practical.
|
|
32
|
+
|
|
33
|
+
Verification:
|
|
34
|
+
- After changing code, run the most targeted relevant tests or checks available.
|
|
35
|
+
- If those reveal a failure related to your change, diagnose and iterate.
|
|
36
|
+
- Expand verification only when the affected surface warrants it.
|
|
37
|
+
- Do not modify tests solely to make an incorrect implementation pass.
|
|
38
|
+
- Do not use hidden benchmark information or assume the expected patch.
|
|
39
|
+
|
|
40
|
+
Available tools:
|
|
41
|
+
- read_file: inspect text or image files.
|
|
42
|
+
- search: find files, symbols, strings, and references.
|
|
43
|
+
- web_search: look up current documentation, error messages, and facts that are not in the repository.
|
|
44
|
+
- patch: modify part of an existing file.
|
|
45
|
+
- write_file: create or replace a complete text file.
|
|
46
|
+
- bash: run repository commands, tests, linters, and other local inspection commands.
|
|
47
|
+
- spawn_agent: delegate an independent, well-scoped investigation when useful.
|
|
48
|
+
- generate_image: create image assets when explicitly required.
|
|
49
|
+
|
|
50
|
+
When done, give a short final answer: what changed and what you verified.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
SUBAGENT_SYSTEM_PROMPT = """You are a subagent owning one delegated task end-to-end.
|
|
54
|
+
|
|
55
|
+
Own the task:
|
|
56
|
+
- Research what you need, act on it, and finish. Do not hand partially finished work back for the parent to redo.
|
|
57
|
+
- Use tools as needed. Prefer evidence from the repo and environment over guessing.
|
|
58
|
+
|
|
59
|
+
Be quick:
|
|
60
|
+
- No preamble. Start working.
|
|
61
|
+
- Keep intermediate chatter minimal; the parent only needs the outcome.
|
|
62
|
+
|
|
63
|
+
Final reply:
|
|
64
|
+
- Keep the final reply short and informative: findings, paths, and the next useful fact — not a novel.
|
|
65
|
+
- Do not ask the user questions or offer follow-ups like "want me to…".
|
|
66
|
+
- Stop when the task is done or context is tight. Prefer a compact final note over another tool loop.
|
|
67
|
+
"""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Agent Client Protocol host
|
|
2
|
+
|
|
3
|
+
`symphony acp [--model PROVIDER:MODEL] [--unattended]` speaks the
|
|
4
|
+
[Agent Client Protocol](https://agentclientprotocol.com) (version 1) as
|
|
5
|
+
JSON-RPC 2.0 over stdio, one message per line. Editors and other ACP clients
|
|
6
|
+
start it as a child process. Standard output carries protocol messages only;
|
|
7
|
+
diagnostics go to standard error.
|
|
8
|
+
|
|
9
|
+
The host is a translator. It owns no agent logic: the same event stream and
|
|
10
|
+
the same `CoreHarness.submit` operations drive the TUI and the JSONL host.
|
|
11
|
+
|
|
12
|
+
## What is implemented
|
|
13
|
+
|
|
14
|
+
| ACP | Symphony |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `initialize` | Returns protocol version 1, `agentInfo`, and capabilities. |
|
|
17
|
+
| `session/new` (`cwd`) | Builds a coding agent for that workspace. The reply's `sessionId` is the Symphony session id. One process can hold several sessions. |
|
|
18
|
+
| `session/prompt` | Runs one turn on the prompt's text. The reply carries `stopReason`: `end_turn`, `cancelled`, `max_turn_requests`, or `max_tokens`. |
|
|
19
|
+
| `session/cancel` | `Interrupt("user_cancel")`. |
|
|
20
|
+
| `session/update` | Sent for `text_delta` (`agent_message_chunk`), `reasoning_delta` (`agent_thought_chunk`), tool events (`tool_call`, `tool_call_update`), `context` (`usage_update`), and injected user input (`user_message_chunk`). |
|
|
21
|
+
| `session/request_permission` | Sent for a `question_asked` event that has choices, such as an approval. The selected option is returned to the run as an `Answer`. |
|
|
22
|
+
| `_symphony/steer` (`sessionId`, `prompt`) | Extension. `UserInput`: adds a user message before the run's next model call. Replies `{"accepted": bool}`. Advertised under `agentCapabilities._meta.symphony.steer`. |
|
|
23
|
+
|
|
24
|
+
## Limits
|
|
25
|
+
|
|
26
|
+
- `session/load` is not implemented (`loadSession` is `false`). Start a new
|
|
27
|
+
session instead.
|
|
28
|
+
- Prompts are flattened to text. `text`, `resource_link`, and text
|
|
29
|
+
`resource` blocks are read; images and audio are not.
|
|
30
|
+
- `mcpServers` in `session/new` is ignored.
|
|
31
|
+
- Only the root agent is narrated. A subagent appears as the parent's
|
|
32
|
+
`spawn_agent` tool call.
|
|
33
|
+
- A question without choices (a free-text `ask_user`) cannot be expressed as
|
|
34
|
+
a permission request, so it takes its default answer.
|
|
35
|
+
- A cancelled or malformed permission outcome denies an approval.
|
|
36
|
+
- Config options and slash commands are not exposed.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent Client Protocol host for editors and other ACP clients.
|
|
2
|
+
|
|
3
|
+
``mapping`` translates events and questions, ``transport`` is JSON-RPC over
|
|
4
|
+
stdio, and ``server`` runs the sessions. The names below stay on this package
|
|
5
|
+
because tests patch them here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from coding_agent.agent import build_agent
|
|
11
|
+
from coding_agent.config.credentials import load_provider_env
|
|
12
|
+
from coding_agent.protocols.acp.mapping import PROTOCOL_VERSION
|
|
13
|
+
from coding_agent.protocols.acp.server import AcpSink, Server, main, serve
|
|
14
|
+
from coding_agent.protocols.acp.transport import Connection, write_message
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AcpSink",
|
|
18
|
+
"Connection",
|
|
19
|
+
"PROTOCOL_VERSION",
|
|
20
|
+
"Server",
|
|
21
|
+
"build_agent",
|
|
22
|
+
"load_provider_env",
|
|
23
|
+
"main",
|
|
24
|
+
"serve",
|
|
25
|
+
"sys",
|
|
26
|
+
"write_message",
|
|
27
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Translate between Symphony's event stream and Agent Client Protocol shapes.
|
|
2
|
+
|
|
3
|
+
Pure functions: no I/O and no agent. ``server`` does the talking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List, Mapping, Sequence
|
|
10
|
+
|
|
11
|
+
from core_harness import HarnessLimitExceeded
|
|
12
|
+
|
|
13
|
+
PROTOCOL_VERSION = 1
|
|
14
|
+
|
|
15
|
+
TOOL_KINDS = {
|
|
16
|
+
"read_file": "read",
|
|
17
|
+
"write_file": "edit",
|
|
18
|
+
"patch": "edit",
|
|
19
|
+
"bash": "execute",
|
|
20
|
+
"search": "search",
|
|
21
|
+
"web_search": "fetch",
|
|
22
|
+
"spawn_agent": "think",
|
|
23
|
+
}
|
|
24
|
+
# How an answer choice reads to a client that only knows ACP option kinds.
|
|
25
|
+
OPTION_KINDS = {
|
|
26
|
+
"allow once": "allow_once",
|
|
27
|
+
"allow": "allow_once",
|
|
28
|
+
"always allow": "allow_always",
|
|
29
|
+
"deny": "reject_once",
|
|
30
|
+
"decline": "reject_once",
|
|
31
|
+
}
|
|
32
|
+
STOP_FOR_LIMIT = {"max_turns": "max_turn_requests", "max_tokens": "max_tokens"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _text(text: Any) -> Dict[str, Any]:
|
|
36
|
+
return {"type": "text", "text": str(text)}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _raw_input(arguments: Any) -> Dict[str, Any] | None:
|
|
40
|
+
"""Tool arguments as an object. Providers send them as a JSON string."""
|
|
41
|
+
if isinstance(arguments, str):
|
|
42
|
+
try:
|
|
43
|
+
arguments = json.loads(arguments)
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
return dict(arguments) if isinstance(arguments, Mapping) else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def session_updates(event_type: str, payload: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
|
50
|
+
"""The ``session/update`` bodies for one Symphony event. Often none.
|
|
51
|
+
|
|
52
|
+
Only the root agent is narrated. A subagent shows up as the parent's
|
|
53
|
+
``spawn_agent`` tool call, not as a second voice in the same message.
|
|
54
|
+
"""
|
|
55
|
+
if payload.get("parent_id"):
|
|
56
|
+
return []
|
|
57
|
+
if event_type == "text_delta":
|
|
58
|
+
return [{"sessionUpdate": "agent_message_chunk", "content": _text(payload.get("delta") or "")}]
|
|
59
|
+
if event_type == "reasoning_delta":
|
|
60
|
+
return [{"sessionUpdate": "agent_thought_chunk", "content": _text(payload.get("delta") or "")}]
|
|
61
|
+
if event_type == "message_injected" and payload.get("source") == "user":
|
|
62
|
+
content = payload.get("content")
|
|
63
|
+
return [{"sessionUpdate": "user_message_chunk", "content": _text(content if isinstance(content, str) else "")}]
|
|
64
|
+
call_id = str(payload.get("tool_call_id") or "")
|
|
65
|
+
if event_type == "tool_call_started" and call_id:
|
|
66
|
+
name = str(payload.get("tool_name") or "tool")
|
|
67
|
+
return [{
|
|
68
|
+
"sessionUpdate": "tool_call",
|
|
69
|
+
"toolCallId": call_id,
|
|
70
|
+
"title": name,
|
|
71
|
+
"kind": TOOL_KINDS.get(name, "other"),
|
|
72
|
+
"status": "pending",
|
|
73
|
+
}]
|
|
74
|
+
if event_type == "tool_call_arguments" and call_id:
|
|
75
|
+
raw = _raw_input(payload.get("arguments"))
|
|
76
|
+
return [] if raw is None else [{"sessionUpdate": "tool_call_update", "toolCallId": call_id, "rawInput": raw}]
|
|
77
|
+
if event_type == "tool_execution_started" and call_id:
|
|
78
|
+
update: Dict[str, Any] = {"sessionUpdate": "tool_call_update", "toolCallId": call_id, "status": "in_progress"}
|
|
79
|
+
raw = _raw_input(payload.get("arguments"))
|
|
80
|
+
if raw is not None:
|
|
81
|
+
update["rawInput"] = raw
|
|
82
|
+
return [update]
|
|
83
|
+
if event_type == "tool_execution_completed" and call_id:
|
|
84
|
+
result = str(payload.get("result") or "")
|
|
85
|
+
return [{
|
|
86
|
+
"sessionUpdate": "tool_call_update",
|
|
87
|
+
"toolCallId": call_id,
|
|
88
|
+
"status": "failed" if payload.get("status") == "error" else "completed",
|
|
89
|
+
"content": [{"type": "content", "content": _text(result)}],
|
|
90
|
+
"rawOutput": {"result": result, "truncated": bool(payload.get("truncated"))},
|
|
91
|
+
}]
|
|
92
|
+
if event_type == "context":
|
|
93
|
+
used, size = payload.get("tokens_used"), payload.get("context_limit")
|
|
94
|
+
if isinstance(used, int) and isinstance(size, int):
|
|
95
|
+
return [{"sessionUpdate": "usage_update", "used": used, "size": size}]
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def permission_request(session_id: str, payload: Mapping[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
"""``session/request_permission`` params for a ``question_asked`` event.
|
|
101
|
+
|
|
102
|
+
Option ids are the choice's position, so the reply maps back exactly.
|
|
103
|
+
"""
|
|
104
|
+
choices = [str(choice) for choice in payload.get("choices") or []]
|
|
105
|
+
tool_call: Dict[str, Any] = {
|
|
106
|
+
"toolCallId": str(payload.get("tool_call_id") or payload.get("request_id") or ""),
|
|
107
|
+
"title": str(payload.get("question") or ""),
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
"sessionId": session_id,
|
|
111
|
+
"toolCall": tool_call,
|
|
112
|
+
"options": [
|
|
113
|
+
{"optionId": str(index), "name": choice, "kind": OPTION_KINDS.get(choice.casefold(), "allow_once")}
|
|
114
|
+
for index, choice in enumerate(choices)
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def permission_answer(outcome: Any, payload: Mapping[str, Any]) -> str:
|
|
120
|
+
"""The answer string for a client's permission outcome.
|
|
121
|
+
|
|
122
|
+
A cancelled or unreadable outcome denies an approval and takes the
|
|
123
|
+
default for any other question.
|
|
124
|
+
"""
|
|
125
|
+
choices = [str(choice) for choice in payload.get("choices") or []]
|
|
126
|
+
fallback = "Deny" if payload.get("kind") == "approval" else str(payload.get("default") or "")
|
|
127
|
+
if not isinstance(outcome, Mapping) or outcome.get("outcome") != "selected":
|
|
128
|
+
return fallback
|
|
129
|
+
try:
|
|
130
|
+
return choices[int(str(outcome.get("optionId")))]
|
|
131
|
+
except (ValueError, IndexError):
|
|
132
|
+
return fallback
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def prompt_text(blocks: Sequence[Any]) -> str:
|
|
136
|
+
"""Flatten ACP content blocks to the text Symphony runs on."""
|
|
137
|
+
parts: List[str] = []
|
|
138
|
+
for block in blocks:
|
|
139
|
+
if not isinstance(block, Mapping):
|
|
140
|
+
continue
|
|
141
|
+
kind = block.get("type")
|
|
142
|
+
if kind == "text":
|
|
143
|
+
parts.append(str(block.get("text") or ""))
|
|
144
|
+
elif kind == "resource_link":
|
|
145
|
+
parts.append(str(block.get("uri") or ""))
|
|
146
|
+
elif kind == "resource":
|
|
147
|
+
resource = block.get("resource")
|
|
148
|
+
if isinstance(resource, Mapping) and isinstance(resource.get("text"), str):
|
|
149
|
+
parts.append(f"{resource.get('uri') or ''}\n{resource['text']}".strip())
|
|
150
|
+
return "\n\n".join(part for part in parts if part)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def stop_reason_for_limit(error: HarnessLimitExceeded) -> str:
|
|
154
|
+
return STOP_FOR_LIMIT.get(str(error.limit), "end_turn")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"PROTOCOL_VERSION",
|
|
159
|
+
"permission_answer",
|
|
160
|
+
"permission_request",
|
|
161
|
+
"prompt_text",
|
|
162
|
+
"session_updates",
|
|
163
|
+
"stop_reason_for_limit",
|
|
164
|
+
]
|