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.
Files changed (129) hide show
  1. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/PKG-INFO +1 -1
  2. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/pyproject.toml +2 -2
  3. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_schema.py +115 -95
  4. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_signature.py +4 -2
  5. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/connection.py +70 -8
  6. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/connection.py +55 -105
  7. agent_client_protocol-0.12.1/src/acp/schema.py +5830 -0
  8. agent_client_protocol-0.12.1/src/acp/task/__init__.py +4 -0
  9. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/task/sender.py +1 -5
  10. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_connection_recovery.py +2 -2
  11. agent_client_protocol-0.12.1/tests/test_core.py +43 -0
  12. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_gen_all.py +11 -0
  13. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_request_error_logging.py +43 -31
  14. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_rpc.py +180 -0
  15. agent_client_protocol-0.12.0/src/acp/schema.py +0 -6782
  16. agent_client_protocol-0.12.0/src/acp/task/__init__.py +0 -44
  17. agent_client_protocol-0.12.0/src/acp/task/dispatcher.py +0 -94
  18. agent_client_protocol-0.12.0/src/acp/task/queue.py +0 -67
  19. agent_client_protocol-0.12.0/src/acp/task/state.py +0 -84
  20. agent_client_protocol-0.12.0/tests/test_core.py +0 -55
  21. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/LICENSE +0 -0
  22. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/README.md +0 -0
  23. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/agent.py +0 -0
  24. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/client.py +0 -0
  25. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/duet.py +0 -0
  26. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/echo_agent.py +0 -0
  27. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/gemini.py +0 -0
  28. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/http_client.py +0 -0
  29. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/http_server.py +0 -0
  30. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/examples/ws_client.py +0 -0
  31. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/__init__.py +0 -0
  32. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_all.py +0 -0
  33. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/scripts/gen_meta.py +0 -0
  34. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/__init__.py +0 -0
  35. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_cookies.py +0 -0
  36. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_deserialize.py +0 -0
  37. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_sse.py +0 -0
  38. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/_transport.py +0 -0
  39. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/__init__.py +0 -0
  40. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/connection.py +0 -0
  41. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/agent/router.py +0 -0
  42. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/__init__.py +0 -0
  43. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/client/router.py +0 -0
  44. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/__init__.py +0 -0
  45. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/permissions.py +0 -0
  46. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/session_state.py +0 -0
  47. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/contrib/tool_calls.py +0 -0
  48. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/core.py +0 -0
  49. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/exceptions.py +0 -0
  50. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/helpers.py +0 -0
  51. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/__init__.py +0 -0
  52. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/asgi.py +0 -0
  53. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/client.py +0 -0
  54. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/protocol.py +0 -0
  55. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/http/server.py +0 -0
  56. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/interfaces.py +0 -0
  57. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/meta.py +0 -0
  58. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/py.typed +0 -0
  59. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/router.py +0 -0
  60. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/stdio.py +0 -0
  61. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/task/supervisor.py +0 -0
  62. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/telemetry.py +0 -0
  63. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/transports.py +0 -0
  64. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/utils.py +0 -0
  65. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/__init__.py +0 -0
  66. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/client.py +0 -0
  67. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/src/acp/ws/server.py +0 -0
  68. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/conftest.py +0 -0
  69. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_permissions.py +0 -0
  70. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_session_state.py +0 -0
  71. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/contrib/test_contrib_tool_calls.py +0 -0
  72. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/cancel_notification.json +0 -0
  73. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_audio.json +0 -0
  74. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_image.json +0 -0
  75. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_blob.json +0 -0
  76. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_link.json +0 -0
  77. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_resource_text.json +0 -0
  78. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/content_text.json +0 -0
  79. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_read_text_file_request.json +0 -0
  80. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_read_text_file_response.json +0 -0
  81. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/fs_write_text_file_request.json +0 -0
  82. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/initialize_request.json +0 -0
  83. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/initialize_response.json +0 -0
  84. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/new_session_request.json +0 -0
  85. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/new_session_response.json +0 -0
  86. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/permission_outcome_cancelled.json +0 -0
  87. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/permission_outcome_selected.json +0 -0
  88. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/prompt_request.json +0 -0
  89. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/request_permission_request.json +0 -0
  90. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/request_permission_response_selected.json +0 -0
  91. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_agent_message_chunk.json +0 -0
  92. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_agent_thought_chunk.json +0 -0
  93. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_config_option_update.json +0 -0
  94. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_plan.json +0 -0
  95. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call.json +0 -0
  96. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_edit.json +0 -0
  97. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_locations_rawinput.json +0 -0
  98. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_read.json +0 -0
  99. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_update_content.json +0 -0
  100. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_tool_call_update_more_fields.json +0 -0
  101. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/session_update_user_message_chunk.json +0 -0
  102. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/set_session_config_option_request.json +0 -0
  103. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/set_session_config_option_response.json +0 -0
  104. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_content_text.json +0 -0
  105. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_diff.json +0 -0
  106. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_diff_no_old.json +0 -0
  107. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/golden/tool_content_terminal.json +0 -0
  108. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/conftest.py +0 -0
  109. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_cookies.py +0 -0
  110. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_fixes.py +0 -0
  111. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_http_client.py +0 -0
  112. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_http_server.py +0 -0
  113. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_loopback.py +0 -0
  114. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_protocol.py +0 -0
  115. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_sse.py +0 -0
  116. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_websocket.py +0 -0
  117. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/http/test_ws_cookies.py +0 -0
  118. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/__init__.py +0 -0
  119. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_cancel_prompt_flow.py +0 -0
  120. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_mcp_servers_optional.py +0 -0
  121. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_permission_flow.py +0 -0
  122. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/real_user/test_stdio_limits.py +0 -0
  123. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_compatibility.py +0 -0
  124. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_deserialize.py +0 -0
  125. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_elicitation_catchall.py +0 -0
  126. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_gemini_example.py +0 -0
  127. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_golden.py +0 -0
  128. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_unstable.py +0 -0
  129. {agent_client_protocol-0.12.0 → agent_client_protocol-0.12.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: agent-client-protocol
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: A Python implement of Agent Client Protocol (ACP, by Zed Industries)
5
5
  Keywords: python
6
6
  Author-Email: Chojan Shang <psiace@apache.org>, Frost Ming <me@frostming.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-client-protocol"
3
- version = "0.12.0"
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.25",
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
- field_pattern = re.compile(rf"(\n\s+{re.escape(field_name)}:\s+Annotated\[[\s\S]*?\n\s+\]\s*)=\s*None")
920
- block = field_pattern.sub(r"\1", block, count=1)
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
- pattern = re.compile(
931
- rf"(class {class_name}\(BaseModel\):.*?\n\s+{field_name}:\s+Annotated\[\s*)Optional\[str],",
932
- re.DOTALL,
933
- )
934
- content, count = pattern.subn(rf"\1Optional[{new_type}],", content)
935
- else:
936
- pattern = re.compile(
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]*?)(?=\n\s{{4}}[A-Za-z_]|$)",
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
- if t.get_origin(annotation) is t.Literal and annotation in self._literals.values():
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
- handler = build_client_router(cast(Client, client), use_unstable_protocol=use_unstable_protocol)
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
- return await request_model(
210
- self._conn,
211
- AGENT_METHODS["session_prompt"],
212
- PromptRequest(prompt=prompt, session_id=session_id, field_meta=kwargs or None),
213
- PromptResponse,
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(