Skip to content

Commit e203729

Browse files
authored
feat: follow agent-client-protocol 0.6.3 (#21)
* feat: bump protocol to 0.6.3 Signed-off-by: Chojan Shang <[email protected]> * fix: make everything happy Signed-off-by: Chojan Shang <[email protected]> * refactor: make gen_schema step by step Signed-off-by: Chojan Shang <[email protected]> * chore: bump to 0.6.3 Signed-off-by: Chojan Shang <[email protected]> --------- Signed-off-by: Chojan Shang <[email protected]>
1 parent 893fcf4 commit e203729

File tree

7 files changed

+178
-39
lines changed

7 files changed

+178
-39
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agent-client-protocol"
3-
version = "0.6.2"
3+
version = "0.6.3"
44
description = "A Python implement of Agent Client Protocol (ACP, by Zed Industries)"
55
authors = [{ name = "Chojan Shang", email = "[email protected]" }]
66
readme = "README.md"

schema/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
refs/tags/v0.6.2
1+
refs/tags/v0.6.3

schema/schema.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,12 @@
708708
},
709709
"ContentBlock": {
710710
"description": "Content blocks represent displayable information in the Agent Client Protocol.\n\nThey provide a structured way to handle various types of user-facing content\u2014whether\nit's text from language models, images for analysis, or embedded resources for context.\n\nContent blocks appear in:\n- User prompts sent via `session/prompt`\n- Language model output streamed through `session/update` notifications\n- Progress updates and results from tool calls\n\nThis structure is compatible with the Model Context Protocol (MCP), enabling\nagents to seamlessly forward content from MCP tool outputs without transformation.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/content)",
711+
"discriminator": {
712+
"propertyName": "type"
713+
},
711714
"oneOf": [
712715
{
713-
"description": "Plain text content\n\nAll agents MUST support text content blocks in prompts.",
716+
"description": "Text content. May be plain text or formatted with Markdown.\n\nAll agents MUST support text content blocks in prompts.\nClients SHOULD render this text as Markdown.",
714717
"properties": {
715718
"_meta": {
716719
"description": "Extension point for implementations"
@@ -1406,7 +1409,10 @@
14061409
"type": "object"
14071410
}
14081411
],
1409-
"description": "Configuration for connecting to an MCP (Model Context Protocol) server.\n\nMCP servers provide tools and context that the agent can use when\nprocessing prompts.\n\nSee protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers)"
1412+
"description": "Configuration for connecting to an MCP (Model Context Protocol) server.\n\nMCP servers provide tools and context that the agent can use when\nprocessing prompts.\n\nSee protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers)",
1413+
"discriminator": {
1414+
"propertyName": "type"
1415+
}
14101416
},
14111417
"ModelId": {
14121418
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for a model.",
@@ -1796,6 +1802,9 @@
17961802
},
17971803
"RequestPermissionOutcome": {
17981804
"description": "The outcome of a permission request.",
1805+
"discriminator": {
1806+
"propertyName": "outcome"
1807+
},
17991808
"oneOf": [
18001809
{
18011810
"description": "The prompt turn was cancelled before the user responded.\n\nWhen a client sends a `session/cancel` notification to cancel an ongoing\nprompt turn, it MUST respond to all pending `session/request_permission`\nrequests with this `Cancelled` outcome.\n\nSee protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)",
@@ -2060,6 +2069,9 @@
20602069
},
20612070
"SessionUpdate": {
20622071
"description": "Different types of updates that can be sent during session processing.\n\nThese updates provide real-time feedback about the agent's progress.\n\nSee protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)",
2072+
"discriminator": {
2073+
"propertyName": "sessionUpdate"
2074+
},
20632075
"oneOf": [
20642076
{
20652077
"description": "A chunk of the user's message being streamed.",
@@ -2530,6 +2542,9 @@
25302542
},
25312543
"ToolCallContent": {
25322544
"description": "Content produced by a tool call.\n\nTool calls can produce different types of content including\nstandard content blocks (text, images) or file diffs.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)",
2545+
"discriminator": {
2546+
"propertyName": "type"
2547+
},
25332548
"oneOf": [
25342549
{
25352550
"description": "Standard content block (text, images, resources).",

scripts/gen_schema.py

Lines changed: 146 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
import argparse
55
import ast
6+
import json
67
import re
78
import shutil
89
import subprocess
910
import sys
1011
from collections.abc import Callable
12+
from dataclasses import dataclass
1113
from pathlib import Path
1214

1315
ROOT = Path(__file__).resolve().parents[1]
@@ -25,6 +27,13 @@
2527
re.DOTALL,
2628
)
2729

30+
STDIO_TYPE_LITERAL = 'Literal["2#-datamodel-code-generator-#-object-#-special-#"]'
31+
STDIO_TYPE_PATTERN = re.compile(
32+
r"^ type:\s*Literal\[['\"]2#-datamodel-code-generator-#-object-#-special-#['\"]\]"
33+
r"(?:\s*=\s*['\"][^'\"]+['\"])?\s*$",
34+
re.MULTILINE,
35+
)
36+
2837
# Map of numbered classes produced by datamodel-code-generator to descriptive names.
2938
# Keep this in sync with the Rust/TypeScript SDK nomenclature.
3039
RENAME_MAP: dict[str, str] = {
@@ -109,6 +118,14 @@
109118
)
110119

111120

121+
@dataclass(frozen=True)
122+
class _ProcessingStep:
123+
"""A named transformation applied to the generated schema content."""
124+
125+
name: str
126+
apply: Callable[[str], str]
127+
128+
112129
def parse_args() -> argparse.Namespace:
113130
parser = argparse.ArgumentParser(description="Generate src/acp/schema.py from the ACP JSON schema.")
114131
parser.add_argument(
@@ -159,68 +176,158 @@ def generate_schema(*, format_output: bool = True) -> None:
159176
]
160177

161178
subprocess.check_call(cmd) # noqa: S603
162-
warnings = rename_types(SCHEMA_OUT)
179+
warnings = postprocess_generated_schema(SCHEMA_OUT)
163180
for warning in warnings:
164181
print(f"Warning: {warning}", file=sys.stderr)
165182

166183
if format_output:
167184
format_with_ruff(SCHEMA_OUT)
168185

169186

170-
def rename_types(output_path: Path) -> list[str]:
187+
def postprocess_generated_schema(output_path: Path) -> list[str]:
171188
if not output_path.exists():
172189
raise RuntimeError(f"Generated schema not found at {output_path}") # noqa: TRY003
173190

174-
content = output_path.read_text(encoding="utf-8")
191+
raw_content = output_path.read_text(encoding="utf-8")
192+
header_block = _build_header_block()
193+
194+
content = _strip_existing_header(raw_content)
195+
content = _remove_backcompat_block(content)
196+
content, leftover_classes = _rename_numbered_models(content)
197+
198+
processing_steps: tuple[_ProcessingStep, ...] = (
199+
_ProcessingStep("apply field overrides", _apply_field_overrides),
200+
_ProcessingStep("apply default overrides", _apply_default_overrides),
201+
_ProcessingStep("normalize stdio literal", _normalize_stdio_model),
202+
_ProcessingStep("attach description comments", _add_description_comments),
203+
_ProcessingStep("ensure custom BaseModel", _ensure_custom_base_model),
204+
)
205+
206+
for step in processing_steps:
207+
content = step.apply(content)
208+
209+
missing_targets = _find_missing_targets(content)
210+
211+
content = _inject_enum_aliases(content)
212+
alias_block = _build_alias_block()
213+
final_content = header_block + content.rstrip() + "\n\n" + alias_block
214+
if not final_content.endswith("\n"):
215+
final_content += "\n"
216+
output_path.write_text(final_content, encoding="utf-8")
217+
218+
warnings: list[str] = []
219+
if leftover_classes:
220+
warnings.append(
221+
"Unrenamed schema models detected: "
222+
+ ", ".join(leftover_classes)
223+
+ ". Update RENAME_MAP in scripts/gen_schema.py."
224+
)
225+
if missing_targets:
226+
warnings.append(
227+
"Renamed schema targets not found after generation: "
228+
+ ", ".join(sorted(missing_targets))
229+
+ ". Check RENAME_MAP or upstream schema changes."
230+
)
231+
warnings.extend(_validate_schema_alignment())
232+
233+
return warnings
234+
175235

236+
def _build_header_block() -> str:
176237
header_lines = ["# Generated from schema/schema.json. Do not edit by hand."]
177238
if VERSION_FILE.exists():
178239
ref = VERSION_FILE.read_text(encoding="utf-8").strip()
179240
if ref:
180241
header_lines.append(f"# Schema ref: {ref}")
242+
return "\n".join(header_lines) + "\n\n"
243+
244+
245+
def _build_alias_block() -> str:
246+
alias_lines = [f"{old} = {new}" for old, new in sorted(RENAME_MAP.items())]
247+
return BACKCOMPAT_MARKER + "\n" + "\n".join(alias_lines) + "\n"
181248

249+
250+
def _strip_existing_header(content: str) -> str:
182251
existing_header = re.match(r"(#.*\n)+", content)
183252
if existing_header:
184-
content = content[existing_header.end() :]
185-
content = content.lstrip("\n")
253+
return content[existing_header.end() :].lstrip("\n")
254+
return content.lstrip("\n")
255+
186256

257+
def _remove_backcompat_block(content: str) -> str:
187258
marker_index = content.find(BACKCOMPAT_MARKER)
188259
if marker_index != -1:
189-
content = content[:marker_index].rstrip()
260+
return content[:marker_index].rstrip()
261+
return content
262+
190263

264+
def _rename_numbered_models(content: str) -> tuple[str, list[str]]:
265+
renamed = content
191266
for old, new in sorted(RENAME_MAP.items(), key=lambda item: len(item[0]), reverse=True):
192267
pattern = re.compile(rf"\b{re.escape(old)}\b")
193-
content = pattern.sub(new, content)
268+
renamed = pattern.sub(new, renamed)
194269

195270
leftover_class_pattern = re.compile(r"^class (\w+\d+)\(", re.MULTILINE)
196-
leftover_classes = sorted(set(leftover_class_pattern.findall(content)))
271+
leftover_classes = sorted(set(leftover_class_pattern.findall(renamed)))
272+
return renamed, leftover_classes
197273

198-
header_block = "\n".join(header_lines) + "\n\n"
199-
content = _apply_field_overrides(content)
200-
content = _apply_default_overrides(content)
201-
content = _add_description_comments(content)
202-
content = _ensure_custom_base_model(content)
203274

204-
alias_lines = [f"{old} = {new}" for old, new in sorted(RENAME_MAP.items())]
205-
alias_block = BACKCOMPAT_MARKER + "\n" + "\n".join(alias_lines) + "\n"
275+
def _find_missing_targets(content: str) -> list[str]:
276+
missing: list[str] = []
277+
for new_name in RENAME_MAP.values():
278+
pattern = re.compile(rf"^class {re.escape(new_name)}\(", re.MULTILINE)
279+
if not pattern.search(content):
280+
missing.append(new_name)
281+
return missing
206282

207-
content = _inject_enum_aliases(content)
208-
content = header_block + content.rstrip() + "\n\n" + alias_block
209-
if not content.endswith("\n"):
210-
content += "\n"
211-
output_path.write_text(content, encoding="utf-8")
212283

284+
def _validate_schema_alignment() -> list[str]:
213285
warnings: list[str] = []
214-
if leftover_classes:
215-
warnings.append(
216-
"Unrenamed schema models detected: "
217-
+ ", ".join(leftover_classes)
218-
+ ". Update RENAME_MAP in scripts/gen_schema.py."
219-
)
286+
if not SCHEMA_JSON.exists():
287+
warnings.append("schema/schema.json missing; unable to validate enum aliases.")
288+
return warnings
220289

290+
try:
291+
schema_enums = _load_schema_enum_literals()
292+
except json.JSONDecodeError as exc:
293+
warnings.append(f"Failed to parse schema/schema.json: {exc}")
294+
return warnings
295+
296+
for enum_name, expected_values in ENUM_LITERAL_MAP.items():
297+
schema_values = schema_enums.get(enum_name)
298+
if schema_values is None:
299+
warnings.append(
300+
f"Enum '{enum_name}' not found in schema.json; update ENUM_LITERAL_MAP or investigate schema changes."
301+
)
302+
continue
303+
if tuple(schema_values) != expected_values:
304+
warnings.append(
305+
f"Enum mismatch for '{enum_name}': schema.json -> {schema_values}, generated aliases -> {expected_values}"
306+
)
221307
return warnings
222308

223309

310+
def _load_schema_enum_literals() -> dict[str, tuple[str, ...]]:
311+
schema_data = json.loads(SCHEMA_JSON.read_text(encoding="utf-8"))
312+
defs = schema_data.get("$defs", {})
313+
enum_literals: dict[str, tuple[str, ...]] = {}
314+
315+
for name, definition in defs.items():
316+
values: list[str] = []
317+
if "enum" in definition:
318+
values = [str(item) for item in definition["enum"]]
319+
elif "oneOf" in definition:
320+
values = [
321+
str(option["const"])
322+
for option in definition.get("oneOf", [])
323+
if isinstance(option, dict) and "const" in option
324+
]
325+
if values:
326+
enum_literals[name] = tuple(values)
327+
328+
return enum_literals
329+
330+
224331
def _ensure_custom_base_model(content: str) -> str:
225332
if "class BaseModel(_BaseModel):" in content:
226333
return content
@@ -323,6 +430,19 @@ def replace_block(
323430
return content
324431

325432

433+
def _normalize_stdio_model(content: str) -> str:
434+
replacement_line = ' type: Literal["stdio"] = "stdio"'
435+
new_content, count = STDIO_TYPE_PATTERN.subn(replacement_line, content)
436+
if count == 0:
437+
return content
438+
if count > 1:
439+
print(
440+
"Warning: multiple stdio type placeholders detected; manual review required.",
441+
file=sys.stderr,
442+
)
443+
return new_content
444+
445+
326446
def _add_description_comments(content: str) -> str:
327447
lines = content.splitlines()
328448
new_lines: list[str] = []

src/acp/meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Generated from schema/meta.json. Do not edit by hand.
2-
# Schema ref: refs/tags/v0.6.2
2+
# Schema ref: refs/tags/v0.6.3
33
AGENT_METHODS = {'authenticate': 'authenticate', 'initialize': 'initialize', 'session_cancel': 'session/cancel', 'session_load': 'session/load', 'session_new': 'session/new', 'session_prompt': 'session/prompt', 'session_set_mode': 'session/set_mode', 'session_set_model': 'session/set_model'}
44
CLIENT_METHODS = {'fs_read_text_file': 'fs/read_text_file', 'fs_write_text_file': 'fs/write_text_file', 'session_request_permission': 'session/request_permission', 'session_update': 'session/update', 'terminal_create': 'terminal/create', 'terminal_kill': 'terminal/kill', 'terminal_output': 'terminal/output', 'terminal_release': 'terminal/release', 'terminal_wait_for_exit': 'terminal/wait_for_exit'}
55
PROTOCOL_VERSION = 1

src/acp/schema.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Generated from schema/schema.json. Do not edit by hand.
2-
# Schema ref: refs/tags/v0.6.2
2+
# Schema ref: refs/tags/v0.6.3
33

44
from __future__ import annotations
55

@@ -244,6 +244,7 @@ class StdioMcpServer(BaseModel):
244244
]
245245
# Human-readable name identifying this MCP server.
246246
name: Annotated[str, Field(description="Human-readable name identifying this MCP server.")]
247+
type: Literal["stdio"] = "stdio"
247248

248249

249250
class ModelInfo(BaseModel):
@@ -336,7 +337,10 @@ class RequestPermissionResponse(BaseModel):
336337
# The user's decision on the permission request.
337338
outcome: Annotated[
338339
Union[DeniedOutcome, AllowedOutcome],
339-
Field(description="The user's decision on the permission request."),
340+
Field(
341+
description="The user's decision on the permission request.",
342+
discriminator="outcome",
343+
),
340344
]
341345

342346

@@ -1195,7 +1199,7 @@ class UserMessageChunk(BaseModel):
11951199
Union[
11961200
TextContentBlock, ImageContentBlock, AudioContentBlock, ResourceContentBlock, EmbeddedResourceContentBlock
11971201
],
1198-
Field(description="A single item of content"),
1202+
Field(description="A single item of content", discriminator="type"),
11991203
]
12001204
sessionUpdate: Literal["user_message_chunk"]
12011205

@@ -1211,7 +1215,7 @@ class AgentMessageChunk(BaseModel):
12111215
Union[
12121216
TextContentBlock, ImageContentBlock, AudioContentBlock, ResourceContentBlock, EmbeddedResourceContentBlock
12131217
],
1214-
Field(description="A single item of content"),
1218+
Field(description="A single item of content", discriminator="type"),
12151219
]
12161220
sessionUpdate: Literal["agent_message_chunk"]
12171221

@@ -1227,7 +1231,7 @@ class AgentThoughtChunk(BaseModel):
12271231
Union[
12281232
TextContentBlock, ImageContentBlock, AudioContentBlock, ResourceContentBlock, EmbeddedResourceContentBlock
12291233
],
1230-
Field(description="A single item of content"),
1234+
Field(description="A single item of content", discriminator="type"),
12311235
]
12321236
sessionUpdate: Literal["agent_thought_chunk"]
12331237

@@ -1238,7 +1242,7 @@ class ContentToolCallContent(BaseModel):
12381242
Union[
12391243
TextContentBlock, ImageContentBlock, AudioContentBlock, ResourceContentBlock, EmbeddedResourceContentBlock
12401244
],
1241-
Field(description="The actual content block."),
1245+
Field(description="The actual content block.", discriminator="type"),
12421246
]
12431247
type: Literal["content"]
12441248

@@ -1457,7 +1461,7 @@ class SessionNotification(BaseModel):
14571461
AvailableCommandsUpdate,
14581462
CurrentModeUpdate,
14591463
],
1460-
Field(description="The actual update content."),
1464+
Field(description="The actual update content.", discriminator="sessionUpdate"),
14611465
]
14621466

14631467

0 commit comments

Comments
 (0)