agent-client-protocol 0.12.0__tar.gz → 0.12.1__tar.gz
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.
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/PKG-INFO +1 -1
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/pyproject.toml +2 -2
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_schema.py +115 -95
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_signature.py +4 -2
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/connection.py +70 -8
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/connection.py +55 -105
- agent_client_protocol-0.12.1/src/acp/schema.py +5830 -0
- agent_client_protocol-0.12.1/src/acp/task/__init__.py +4 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/task/sender.py +1 -5
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_connection_recovery.py +2 -2
- agent_client_protocol-0.12.1/tests/test_core.py +43 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_gen_all.py +11 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_request_error_logging.py +43 -31
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_rpc.py +180 -0
- agent_client_protocol-0.12.0/src/acp/schema.py +0 -6782
- agent_client_protocol-0.12.0/src/acp/task/__init__.py +0 -44
- agent_client_protocol-0.12.0/src/acp/task/dispatcher.py +0 -94
- agent_client_protocol-0.12.0/src/acp/task/queue.py +0 -67
- agent_client_protocol-0.12.0/src/acp/task/state.py +0 -84
- agent_client_protocol-0.12.0/tests/test_core.py +0 -55
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/LICENSE +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/README.md +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/agent.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/duet.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/echo_agent.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/gemini.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/http_client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/http_server.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/ws_client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_all.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_meta.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_cookies.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_deserialize.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_sse.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_transport.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/connection.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/router.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/router.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/permissions.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/session_state.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/tool_calls.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/core.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/exceptions.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/helpers.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/asgi.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/protocol.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/server.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/interfaces.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/meta.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/py.typed +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/router.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/stdio.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/task/supervisor.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/telemetry.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/transports.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/utils.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/server.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/conftest.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_permissions.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_session_state.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_tool_calls.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/cancel_notification.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_audio.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_image.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_blob.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_link.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_text.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_text.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_read_text_file_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_read_text_file_response.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_write_text_file_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/initialize_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/initialize_response.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/new_session_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/new_session_response.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/permission_outcome_cancelled.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/permission_outcome_selected.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/prompt_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/request_permission_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/request_permission_response_selected.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_agent_message_chunk.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_agent_thought_chunk.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_config_option_update.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_plan.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_edit.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_locations_rawinput.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_read.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_update_content.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_update_more_fields.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_user_message_chunk.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/set_session_config_option_request.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/set_session_config_option_response.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_content_text.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_diff.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_diff_no_old.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_terminal.json +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/conftest.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_cookies.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_fixes.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_http_client.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_http_server.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_loopback.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_protocol.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_sse.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_websocket.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_ws_cookies.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/__init__.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_cancel_prompt_flow.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_mcp_servers_optional.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_permission_flow.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_stdio_limits.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_compatibility.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_deserialize.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_elicitation_catchall.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_gemini_example.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_golden.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_unstable.py +0 -0
- {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-client-protocol"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.1"
|
|
4
4
|
description = "A Python implement of Agent Client Protocol (ACP, by Zed Industries)"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Chojan Shang", email = "psiace@apache.org" },
|
|
@@ -43,7 +43,7 @@ http = [
|
|
|
43
43
|
|
|
44
44
|
[dependency-groups]
|
|
45
45
|
dev = [
|
|
46
|
-
"datamodel-code-generator>=0.
|
|
46
|
+
"datamodel-code-generator>=0.71.0",
|
|
47
47
|
"pytest>=7.2.0",
|
|
48
48
|
"pytest-asyncio>=0.21.0",
|
|
49
49
|
"tox-uv>=1.11.3",
|
|
@@ -20,13 +20,6 @@ SCHEMA_JSON = SCHEMA_DIR / "schema.json"
|
|
|
20
20
|
VERSION_FILE = SCHEMA_DIR / "VERSION"
|
|
21
21
|
SCHEMA_OUT = ROOT / "src" / "acp" / "schema.py"
|
|
22
22
|
|
|
23
|
-
# Pattern caches used when post-processing generated schema.
|
|
24
|
-
FIELD_DECLARATION_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\s*:")
|
|
25
|
-
DESCRIPTION_PATTERN = re.compile(
|
|
26
|
-
r"description\s*=\s*(?P<prefix>[rRbBuU]*)?(?P<quote>'''|\"\"\"|'|\")(?P<value>.*?)(?P=quote)",
|
|
27
|
-
re.DOTALL,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
23
|
STDIO_TYPE_LITERAL = 'Literal["2#-datamodel-code-generator-#-object-#-special-#"]'
|
|
31
24
|
MODELS_TO_REMOVE = [
|
|
32
25
|
"AgentClientProtocol",
|
|
@@ -137,6 +130,31 @@ ENUM_LITERAL_MAP: dict[str, tuple[str, ...]] = {
|
|
|
137
130
|
"ToolKind": ("read", "edit", "delete", "move", "search", "execute", "think", "fetch", "switch_mode", "other"),
|
|
138
131
|
}
|
|
139
132
|
|
|
133
|
+
# datamodel-code-generator 0.64 promotes referenced string enums to Enum classes.
|
|
134
|
+
# Keep the existing Python API, where these schema types are plain strings; the
|
|
135
|
+
# selected public fields below are narrowed back to the named Literal aliases.
|
|
136
|
+
STRING_ENUM_TYPES = (
|
|
137
|
+
*ENUM_LITERAL_MAP,
|
|
138
|
+
"ElicitationSchemaType",
|
|
139
|
+
"NesDiagnosticSeverity",
|
|
140
|
+
"NesRejectReason",
|
|
141
|
+
"NesTriggerKind",
|
|
142
|
+
"PositionEncodingKind",
|
|
143
|
+
"Role",
|
|
144
|
+
"StringFormat",
|
|
145
|
+
"TextDocumentSyncKind",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Preserve RootModel classes that existed in the generated public surface before
|
|
149
|
+
# 0.64; other unreferenced RootModels are intermediates left after collapsing.
|
|
150
|
+
PUBLIC_ROOT_MODELS = {
|
|
151
|
+
"AgentResponse",
|
|
152
|
+
"ClientResponse",
|
|
153
|
+
"ElicitationContentValue",
|
|
154
|
+
"ElicitationFormMode",
|
|
155
|
+
"ElicitationUrlMode",
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
FIELD_TYPE_OVERRIDES: tuple[tuple[str, str, str, bool], ...] = (
|
|
141
159
|
("PermissionOption", "kind", "PermissionOptionKind", False),
|
|
142
160
|
("PlanEntry", "priority", "PlanEntryPriority", False),
|
|
@@ -259,7 +277,16 @@ def generate_schema() -> None:
|
|
|
259
277
|
"--collapse-root-models",
|
|
260
278
|
"--output-model-type",
|
|
261
279
|
"pydantic_v2.BaseModel",
|
|
280
|
+
"--no-use-specialized-enum",
|
|
281
|
+
"--no-use-standard-collections",
|
|
282
|
+
"--no-use-union-operator",
|
|
283
|
+
"--type-overrides",
|
|
284
|
+
json.dumps(dict.fromkeys(STRING_ENUM_TYPES, "builtins.str")),
|
|
285
|
+
"--formatters",
|
|
286
|
+
"black",
|
|
287
|
+
"isort",
|
|
262
288
|
"--use-annotated",
|
|
289
|
+
"--use-field-description",
|
|
263
290
|
"--snake-case-field",
|
|
264
291
|
]
|
|
265
292
|
|
|
@@ -474,6 +501,9 @@ def postprocess_generated_schema(output_path: Path) -> list[str]:
|
|
|
474
501
|
header_block = _build_header_block()
|
|
475
502
|
|
|
476
503
|
content = _strip_existing_header(raw_content)
|
|
504
|
+
# Type overrides for builtins are rendered as imports in 0.64, but the
|
|
505
|
+
# annotations should continue to use Python's builtin `str` directly.
|
|
506
|
+
content = content.replace("from builtins import str\n", "")
|
|
477
507
|
content = _remove_unused_models(content)
|
|
478
508
|
content, leftover_classes = _rename_numbered_models(content)
|
|
479
509
|
|
|
@@ -481,8 +511,8 @@ def postprocess_generated_schema(output_path: Path) -> list[str]:
|
|
|
481
511
|
_ProcessingStep("apply field overrides", _apply_field_overrides),
|
|
482
512
|
_ProcessingStep("apply default overrides", _apply_default_overrides),
|
|
483
513
|
_ProcessingStep("restore required nullable fields", _restore_required_nullable_fields),
|
|
484
|
-
_ProcessingStep("attach description comments", _add_description_comments),
|
|
485
514
|
_ProcessingStep("ensure custom BaseModel", _ensure_custom_base_model),
|
|
515
|
+
_ProcessingStep("enable RootModel attribute docstrings", _enable_root_model_attribute_docstrings),
|
|
486
516
|
_ProcessingStep("inject field validators", _inject_field_validators),
|
|
487
517
|
_ProcessingStep("inject deserialize defaults", _inject_deserialize_defaults),
|
|
488
518
|
_ProcessingStep("inject schema aliases", _inject_schema_aliases),
|
|
@@ -494,6 +524,7 @@ def postprocess_generated_schema(output_path: Path) -> list[str]:
|
|
|
494
524
|
missing_targets = _find_missing_targets(content)
|
|
495
525
|
|
|
496
526
|
content = _inject_enum_aliases(content)
|
|
527
|
+
content = _remove_unreferenced_root_models(content)
|
|
497
528
|
final_content = header_block + content.rstrip() + "\n"
|
|
498
529
|
if not final_content.endswith("\n"):
|
|
499
530
|
final_content += "\n"
|
|
@@ -646,7 +677,7 @@ def _ensure_custom_base_model(content: str) -> str:
|
|
|
646
677
|
lines[idx] = "from pydantic import " + ", ".join(new_imports)
|
|
647
678
|
to_insert = textwrap.dedent("""\
|
|
648
679
|
class BaseModel(_BaseModel):
|
|
649
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
680
|
+
model_config = ConfigDict(populate_by_name=True, use_attribute_docstrings=True)
|
|
650
681
|
|
|
651
682
|
def __getattr__(self, item: str) -> Any:
|
|
652
683
|
if item.lower() != item:
|
|
@@ -662,6 +693,24 @@ def _ensure_custom_base_model(content: str) -> str:
|
|
|
662
693
|
return "\n".join(lines) + "\n"
|
|
663
694
|
|
|
664
695
|
|
|
696
|
+
def _enable_root_model_attribute_docstrings(content: str) -> str:
|
|
697
|
+
lines = content.splitlines(keepends=True)
|
|
698
|
+
tree = ast.parse(content)
|
|
699
|
+
insertion_points = [
|
|
700
|
+
node.body[0].lineno - 1
|
|
701
|
+
for node in tree.body
|
|
702
|
+
if isinstance(node, ast.ClassDef)
|
|
703
|
+
and node.body
|
|
704
|
+
and any(
|
|
705
|
+
isinstance(base, ast.Subscript) and isinstance(base.value, ast.Name) and base.value.id == "RootModel"
|
|
706
|
+
for base in node.bases
|
|
707
|
+
)
|
|
708
|
+
]
|
|
709
|
+
for line_index in reversed(insertion_points):
|
|
710
|
+
lines.insert(line_index, " model_config = ConfigDict(use_attribute_docstrings=True)\n\n")
|
|
711
|
+
return "".join(lines)
|
|
712
|
+
|
|
713
|
+
|
|
665
714
|
def _ensure_pydantic_import(content: str, name: str) -> str:
|
|
666
715
|
"""Add *name* to the ``from pydantic import ...`` line if not already present."""
|
|
667
716
|
lines = content.splitlines()
|
|
@@ -916,8 +965,14 @@ def _restore_required_nullable_fields(content: str, schema: dict[str, Any] | Non
|
|
|
916
965
|
def restore_block(match: re.Match[str], _field_names: list[str] = field_names) -> str:
|
|
917
966
|
header, block = match.group(1), match.group(2)
|
|
918
967
|
for field_name in _field_names:
|
|
919
|
-
|
|
920
|
-
|
|
968
|
+
field_patterns = (
|
|
969
|
+
re.compile(rf"(\n\s+{re.escape(field_name)}:[^\n]*?)\s*=\s*None(?=\n)"),
|
|
970
|
+
re.compile(rf"(\n\s+{re.escape(field_name)}:[^\n]*\[\s*\n[\s\S]*?\n\s+\]\s*)=\s*None"),
|
|
971
|
+
)
|
|
972
|
+
for field_pattern in field_patterns:
|
|
973
|
+
block, count = field_pattern.subn(r"\1", block, count=1)
|
|
974
|
+
if count:
|
|
975
|
+
break
|
|
921
976
|
return header + block
|
|
922
977
|
|
|
923
978
|
content = class_pattern.sub(restore_block, content, count=1)
|
|
@@ -926,18 +981,14 @@ def _restore_required_nullable_fields(content: str, schema: dict[str, Any] | Non
|
|
|
926
981
|
|
|
927
982
|
def _apply_field_overrides(content: str) -> str:
|
|
928
983
|
for class_name, field_name, new_type, optional in FIELD_TYPE_OVERRIDES:
|
|
929
|
-
if optional
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
)
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
rf"(class {class_name}\(BaseModel\):.*?\n\s+{field_name}:\s+Annotated\[\s*)str,",
|
|
938
|
-
re.DOTALL,
|
|
939
|
-
)
|
|
940
|
-
content, count = pattern.subn(rf"\1{new_type},", content)
|
|
984
|
+
old_type = "Optional[str]" if optional else "str"
|
|
985
|
+
replacement_type = f"Optional[{new_type}]" if optional else new_type
|
|
986
|
+
pattern = re.compile(
|
|
987
|
+
rf"(class {re.escape(class_name)}\(BaseModel\):.*?\n\s+{re.escape(field_name)}:\s+"
|
|
988
|
+
rf"(?:Annotated\[\s*)?){re.escape(old_type)}(?=\s*(?:,|=|\n))",
|
|
989
|
+
re.DOTALL,
|
|
990
|
+
)
|
|
991
|
+
content, count = pattern.subn(rf"\g<1>{replacement_type}", content, count=1)
|
|
941
992
|
if count == 0:
|
|
942
993
|
print(
|
|
943
994
|
f"Warning: failed to apply type override for {class_name}.{field_name} -> {new_type}",
|
|
@@ -963,7 +1014,8 @@ def _apply_default_overrides(content: str) -> str:
|
|
|
963
1014
|
field_patterns: tuple[tuple[re.Pattern[str], Callable[[re.Match[str]], str]], ...] = (
|
|
964
1015
|
(
|
|
965
1016
|
re.compile(
|
|
966
|
-
rf"(\n\s+{_field_name}:.*?\]\s*=\s*)([\s\S]*?)
|
|
1017
|
+
rf"(\n\s+{_field_name}:.*?\]\s*=\s*)([\s\S]*?)"
|
|
1018
|
+
rf"(?=\n\s{{4}}(?:[A-Za-z_][A-Za-z0-9_]*\s*:|[rRuUbBfF]*(?:'''|\"\"\"))|$)",
|
|
967
1019
|
re.DOTALL,
|
|
968
1020
|
),
|
|
969
1021
|
lambda m, _rep=_replacement: m.group(1) + _rep,
|
|
@@ -995,77 +1047,6 @@ def _apply_default_overrides(content: str) -> str:
|
|
|
995
1047
|
return content
|
|
996
1048
|
|
|
997
1049
|
|
|
998
|
-
def _add_description_comments(content: str) -> str:
|
|
999
|
-
lines = content.splitlines()
|
|
1000
|
-
new_lines: list[str] = []
|
|
1001
|
-
index = 0
|
|
1002
|
-
|
|
1003
|
-
while index < len(lines):
|
|
1004
|
-
line = lines[index]
|
|
1005
|
-
stripped = line.lstrip()
|
|
1006
|
-
indent = len(line) - len(stripped)
|
|
1007
|
-
|
|
1008
|
-
if indent == 4 and FIELD_DECLARATION_PATTERN.match(stripped or ""):
|
|
1009
|
-
block_lines, next_index = _collect_field_block(lines, index, indent)
|
|
1010
|
-
block_text = "\n".join(block_lines)
|
|
1011
|
-
description = _extract_description(block_text)
|
|
1012
|
-
|
|
1013
|
-
if description:
|
|
1014
|
-
indent_str = " " * indent
|
|
1015
|
-
comment_lines = [
|
|
1016
|
-
f"{indent_str}# {comment_line}" if comment_line else f"{indent_str}#"
|
|
1017
|
-
for comment_line in description.splitlines()
|
|
1018
|
-
]
|
|
1019
|
-
if comment_lines:
|
|
1020
|
-
new_lines.extend(comment_lines)
|
|
1021
|
-
|
|
1022
|
-
new_lines.extend(block_lines)
|
|
1023
|
-
index = next_index
|
|
1024
|
-
continue
|
|
1025
|
-
|
|
1026
|
-
new_lines.append(line)
|
|
1027
|
-
index += 1
|
|
1028
|
-
|
|
1029
|
-
return "\n".join(new_lines)
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
def _collect_field_block(lines: list[str], start: int, indent: int) -> tuple[list[str], int]:
|
|
1033
|
-
block: list[str] = []
|
|
1034
|
-
index = start
|
|
1035
|
-
|
|
1036
|
-
while index < len(lines):
|
|
1037
|
-
current_line = lines[index]
|
|
1038
|
-
current_indent = len(current_line) - len(current_line.lstrip())
|
|
1039
|
-
if index != start and current_line.strip() and current_indent <= indent:
|
|
1040
|
-
break
|
|
1041
|
-
|
|
1042
|
-
block.append(current_line)
|
|
1043
|
-
index += 1
|
|
1044
|
-
|
|
1045
|
-
return block, index
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
def _extract_description(block_text: str) -> str | None:
|
|
1049
|
-
match = DESCRIPTION_PATTERN.search(block_text)
|
|
1050
|
-
if not match:
|
|
1051
|
-
return None
|
|
1052
|
-
|
|
1053
|
-
prefix = match.group("prefix") or ""
|
|
1054
|
-
quote = match.group("quote")
|
|
1055
|
-
value = match.group("value")
|
|
1056
|
-
literal = f"{prefix}{quote}{value}{quote}"
|
|
1057
|
-
|
|
1058
|
-
# datamodel-code-generator emits standard string literals, but fall back to raw text on parse errors.
|
|
1059
|
-
try:
|
|
1060
|
-
parsed = ast.literal_eval(literal)
|
|
1061
|
-
except (SyntaxError, ValueError):
|
|
1062
|
-
return value.replace("\\n", "\n")
|
|
1063
|
-
|
|
1064
|
-
if isinstance(parsed, str):
|
|
1065
|
-
return parsed
|
|
1066
|
-
return str(parsed)
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
1050
|
def _inject_enum_aliases(content: str) -> str:
|
|
1070
1051
|
enum_lines = [
|
|
1071
1052
|
f"{name} = Literal[{', '.join(repr(value) for value in values)}]" for name, values in ENUM_LITERAL_MAP.items()
|
|
@@ -1080,6 +1061,45 @@ def _inject_enum_aliases(content: str) -> str:
|
|
|
1080
1061
|
return content[:insertion_point] + block + content[insertion_point:]
|
|
1081
1062
|
|
|
1082
1063
|
|
|
1064
|
+
def _remove_unreferenced_root_models(content: str) -> str:
|
|
1065
|
+
tree = ast.parse(content)
|
|
1066
|
+
root_models = {
|
|
1067
|
+
node.name: node
|
|
1068
|
+
for node in tree.body
|
|
1069
|
+
if isinstance(node, ast.ClassDef)
|
|
1070
|
+
and any(
|
|
1071
|
+
isinstance(base, ast.Subscript) and isinstance(base.value, ast.Name) and base.value.id == "RootModel"
|
|
1072
|
+
for base in node.bases
|
|
1073
|
+
)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
referenced_roots = set(PUBLIC_ROOT_MODELS)
|
|
1077
|
+
root_dependencies: dict[str, set[str]] = {}
|
|
1078
|
+
for statement in tree.body:
|
|
1079
|
+
loaded_names = {
|
|
1080
|
+
node.id
|
|
1081
|
+
for node in ast.walk(statement)
|
|
1082
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id in root_models
|
|
1083
|
+
}
|
|
1084
|
+
if isinstance(statement, ast.ClassDef) and statement.name in root_models:
|
|
1085
|
+
root_dependencies[statement.name] = loaded_names
|
|
1086
|
+
else:
|
|
1087
|
+
referenced_roots.update(loaded_names)
|
|
1088
|
+
|
|
1089
|
+
pending = list(referenced_roots)
|
|
1090
|
+
while pending:
|
|
1091
|
+
root_name = pending.pop()
|
|
1092
|
+
for dependency in root_dependencies.get(root_name, set()) - referenced_roots:
|
|
1093
|
+
referenced_roots.add(dependency)
|
|
1094
|
+
pending.append(dependency)
|
|
1095
|
+
|
|
1096
|
+
unused_models = [model for name, model in root_models.items() if name not in referenced_roots]
|
|
1097
|
+
lines = content.splitlines(keepends=True)
|
|
1098
|
+
for model in sorted(unused_models, key=lambda item: item.lineno, reverse=True):
|
|
1099
|
+
del lines[model.lineno - 1 : model.end_lineno]
|
|
1100
|
+
return re.sub(r"\n{4,}", "\n\n\n", "".join(lines))
|
|
1101
|
+
|
|
1102
|
+
|
|
1083
1103
|
def _remove_unused_models(content: str) -> str:
|
|
1084
1104
|
for model_name in MODELS_TO_REMOVE:
|
|
1085
1105
|
pattern = re.compile(
|
|
@@ -127,7 +127,10 @@ class NodeTransformer(ast.NodeTransformer):
|
|
|
127
127
|
return arg, default
|
|
128
128
|
|
|
129
129
|
def _format_annotation(self, annotation: t.Any) -> ast.expr:
|
|
130
|
-
|
|
130
|
+
origin = t.get_origin(annotation)
|
|
131
|
+
if origin is t.Annotated:
|
|
132
|
+
return self._format_annotation(t.get_args(annotation)[0])
|
|
133
|
+
if origin is t.Literal and annotation in self._literals.values():
|
|
131
134
|
name = next(name for name, value in self._literals.items() if value is annotation)
|
|
132
135
|
self._add_schema_import(name)
|
|
133
136
|
return ast.Name(id=name)
|
|
@@ -139,7 +142,6 @@ class NodeTransformer(ast.NodeTransformer):
|
|
|
139
142
|
self._add_schema_import(annotation.__name__)
|
|
140
143
|
return ast.Name(id=annotation.__name__)
|
|
141
144
|
elif args := t.get_args(annotation):
|
|
142
|
-
origin = t.get_origin(annotation)
|
|
143
145
|
return ast.Subscript(
|
|
144
146
|
value=self._format_annotation(origin),
|
|
145
147
|
slice=ast.Tuple(elts=[self._format_annotation(arg) for arg in args], ctx=ast.Load())
|
|
@@ -2,12 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
+
from contextvars import ContextVar
|
|
5
6
|
from typing import Any, cast, final
|
|
6
7
|
|
|
7
8
|
from .._transport import Transport
|
|
8
9
|
from ..connection import Connection
|
|
10
|
+
from ..exceptions import RequestError
|
|
9
11
|
from ..interfaces import Agent, Client
|
|
10
|
-
from ..meta import AGENT_METHODS
|
|
12
|
+
from ..meta import AGENT_METHODS, CLIENT_METHODS
|
|
13
|
+
from ..router import _resolve_handler, _warn_legacy_handler
|
|
11
14
|
from ..schema import (
|
|
12
15
|
AcpMcpServer,
|
|
13
16
|
AudioContentBlock,
|
|
@@ -37,6 +40,7 @@ from ..schema import (
|
|
|
37
40
|
ResourceContentBlock,
|
|
38
41
|
ResumeSessionRequest,
|
|
39
42
|
ResumeSessionResponse,
|
|
43
|
+
SessionNotification,
|
|
40
44
|
SetSessionConfigOptionBooleanRequest,
|
|
41
45
|
SetSessionConfigOptionResponse,
|
|
42
46
|
SetSessionConfigOptionSelectRequest,
|
|
@@ -52,6 +56,56 @@ __all__ = ["ClientSideConnection"]
|
|
|
52
56
|
_CLIENT_CONNECTION_ERROR = "ClientSideConnection requires asyncio StreamWriter/StreamReader"
|
|
53
57
|
|
|
54
58
|
|
|
59
|
+
class _SessionUpdateTracker:
|
|
60
|
+
"""Client proxy that tracks in-flight session updates."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, client: Client) -> None:
|
|
63
|
+
self._client = client
|
|
64
|
+
self._session_update, self._session_update_attr, self._legacy_session_update = _resolve_handler(
|
|
65
|
+
client, "session_update"
|
|
66
|
+
)
|
|
67
|
+
self._pending: dict[str, set[asyncio.Future[None]]] = {}
|
|
68
|
+
self._current_update: ContextVar[asyncio.Future[None] | None] = ContextVar(
|
|
69
|
+
"acp_current_session_update", default=None
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None:
|
|
73
|
+
completed: asyncio.Future[None] = asyncio.get_running_loop().create_future()
|
|
74
|
+
pending = self._pending.setdefault(session_id, set())
|
|
75
|
+
pending.add(completed)
|
|
76
|
+
token = self._current_update.set(completed)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
if self._session_update is None:
|
|
80
|
+
raise RequestError.method_not_found(CLIENT_METHODS["session_update"])
|
|
81
|
+
if self._legacy_session_update:
|
|
82
|
+
_warn_legacy_handler(self._client, self._session_update_attr)
|
|
83
|
+
notification = SessionNotification(session_id=session_id, update=update, field_meta=kwargs or None)
|
|
84
|
+
await self._session_update(notification)
|
|
85
|
+
else:
|
|
86
|
+
await self._session_update(session_id=session_id, update=update, **kwargs)
|
|
87
|
+
finally:
|
|
88
|
+
self._current_update.reset(token)
|
|
89
|
+
if not completed.done():
|
|
90
|
+
completed.set_result(None)
|
|
91
|
+
pending.discard(completed)
|
|
92
|
+
if not pending:
|
|
93
|
+
self._pending.pop(session_id, None)
|
|
94
|
+
|
|
95
|
+
async def wait(self, session_id: str) -> None:
|
|
96
|
+
# Snapshot before yielding so updates received after the response are
|
|
97
|
+
# not associated with this prompt.
|
|
98
|
+
current = self._current_update.get()
|
|
99
|
+
notifications = tuple(
|
|
100
|
+
completed for completed in self._pending.get(session_id, set()) if completed is not current
|
|
101
|
+
)
|
|
102
|
+
if notifications:
|
|
103
|
+
await asyncio.gather(*(asyncio.shield(completed) for completed in notifications))
|
|
104
|
+
|
|
105
|
+
def __getattr__(self, name: str) -> Any:
|
|
106
|
+
return getattr(self._client, name)
|
|
107
|
+
|
|
108
|
+
|
|
55
109
|
@final
|
|
56
110
|
@compatible_class
|
|
57
111
|
class ClientSideConnection:
|
|
@@ -69,7 +123,9 @@ class ClientSideConnection:
|
|
|
69
123
|
**connection_kwargs: Any,
|
|
70
124
|
) -> None:
|
|
71
125
|
client = to_client(self) if callable(to_client) else to_client
|
|
72
|
-
|
|
126
|
+
self._session_updates = _SessionUpdateTracker(cast(Client, client))
|
|
127
|
+
handler = build_client_router(cast(Client, self._session_updates), use_unstable_protocol=use_unstable_protocol)
|
|
128
|
+
|
|
73
129
|
if isinstance(input_stream, Transport):
|
|
74
130
|
if output_stream is not None:
|
|
75
131
|
raise TypeError(_CLIENT_CONNECTION_ERROR)
|
|
@@ -206,12 +262,18 @@ class ClientSideConnection:
|
|
|
206
262
|
],
|
|
207
263
|
**kwargs: Any,
|
|
208
264
|
) -> PromptResponse:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
265
|
+
try:
|
|
266
|
+
response = await request_model(
|
|
267
|
+
self._conn,
|
|
268
|
+
AGENT_METHODS["session_prompt"],
|
|
269
|
+
PromptRequest(prompt=prompt, session_id=session_id, field_meta=kwargs or None),
|
|
270
|
+
PromptResponse,
|
|
271
|
+
)
|
|
272
|
+
except Exception:
|
|
273
|
+
await self._session_updates.wait(session_id)
|
|
274
|
+
raise
|
|
275
|
+
await self._session_updates.wait(session_id)
|
|
276
|
+
return response
|
|
215
277
|
|
|
216
278
|
@param_model(ForkSessionRequest)
|
|
217
279
|
async def fork_session(
|