Skip to content

Commit fa105cf

Browse files
Merge origin/main into feat/acp-agent-tcp-transport
Resolve conflicts between TCP transport feature and upstream changes (ACPAgent refactoring from #2133, ask_agent from #2145). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents aead27b + 43ee32f commit fa105cf

File tree

76 files changed

+5496
-2225
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+5496
-2225
lines changed

.agents/skills/custom-codereview-guide.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ Examples of straightforward and low-risk PRs you should approve (non-exhaustive)
2929
- **Test-only changes**: Adding or updating tests without changing production code
3030
- **Dependency updates**: Version bumps with passing CI
3131

32+
### When NOT to APPROVE - Blocking Issues
33+
34+
**DO NOT APPROVE** PRs that have any of the following issues:
35+
36+
- **Package version bumps in non-release PRs**: If any `pyproject.toml` file has changes to the `version` field (e.g., `version = "1.12.0"``version = "1.13.0"`), and the PR is NOT explicitly a release PR (title/description doesn't indicate it's a release), **DO NOT APPROVE**. Version numbers should only be changed in dedicated release PRs managed by maintainers.
37+
- Check: Look for changes to `version = "..."` in any `*/pyproject.toml` files
38+
- Exception: PRs with titles like "release: v1.x.x" or "chore: bump version to 1.x.x" from maintainers
39+
3240
Examples:
3341
- A PR adding a new model to `resolve_model_config.py` or `verified_models.py` with corresponding test updates
3442
- A PR adding documentation notes to docstrings clarifying method behavior (e.g., security considerations, bypass behaviors)

.github/run-eval/resolve_model_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
"display_name": "Gemini 3 Flash",
8787
"llm_config": {"model": "litellm_proxy/gemini-3-flash-preview"},
8888
},
89+
"gemini-3.1-pro": {
90+
"id": "gemini-3.1-pro",
91+
"display_name": "Gemini 3.1 Pro",
92+
"llm_config": {"model": "litellm_proxy/gemini-3.1-pro-preview"},
93+
},
8994
"gpt-5.2": {
9095
"id": "gpt-5.2",
9196
"display_name": "GPT-5.2",

.github/scripts/check_sdk_api_breakage.py

Lines changed: 158 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import ast
3131
import json
3232
import os
33+
import re
3334
import sys
3435
import tomllib
3536
import urllib.request
@@ -147,6 +148,38 @@ def ensure_griffe() -> None:
147148
raise SystemExit(1)
148149

149150

151+
def _is_field_metadata_only_change(old_val: object, new_val: object) -> bool:
152+
"""Check if the change is only in Field metadata (description, title, etc.).
153+
154+
Field metadata parameters like ``description``, ``title``, and ``examples``
155+
don't affect runtime behavior - they're documentation-only. Changes to these
156+
should not be considered breaking API changes.
157+
158+
Returns:
159+
True if both values are Field() calls and only metadata parameters differ.
160+
"""
161+
old_str = str(old_val)
162+
new_str = str(new_val)
163+
164+
if not (old_str.startswith("Field(") and new_str.startswith("Field(")):
165+
return False
166+
167+
# Metadata parameters that don't affect runtime behavior
168+
# See https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field
169+
metadata_params = ["description", "title", "examples", "json_schema_extra"]
170+
171+
old_normalized = old_str
172+
new_normalized = new_str
173+
174+
for param in metadata_params:
175+
# Pattern to match param='...' or param="..." with simple string values
176+
pattern = rf'{param}\s*=\s*([\'"])([^\'"]*?)\1'
177+
old_normalized = re.sub(pattern, f"{param}=PLACEHOLDER", old_normalized)
178+
new_normalized = re.sub(pattern, f"{param}=PLACEHOLDER", new_normalized)
179+
180+
return old_normalized == new_normalized
181+
182+
150183
def _collect_breakages_pairs(
151184
objs: Iterable[tuple[object, object]],
152185
*,
@@ -160,40 +193,81 @@ def _collect_breakages_pairs(
160193
Returns:
161194
(breakages, undeprecated_removals)
162195
"""
196+
163197
import griffe
164-
from griffe import BreakageKind, ExplanationStyle, Kind
198+
from griffe import Alias, AliasResolutionError, BreakageKind, ExplanationStyle, Kind
165199

166-
breakages = []
200+
breakages: list[object] = []
167201
undeprecated_removals = 0
168202

169203
for old, new in objs:
170-
for br in griffe.find_breaking_changes(old, new):
171-
obj = getattr(br, "obj", None)
172-
if not getattr(obj, "is_public", True):
173-
continue
174-
175-
print(br.explain(style=ExplanationStyle.GITHUB))
176-
breakages.append(br)
177-
178-
if br.kind != BreakageKind.OBJECT_REMOVED:
179-
continue
180-
181-
parent = getattr(obj, "parent", None)
182-
if getattr(parent, "kind", None) != Kind.CLASS:
183-
continue
184-
185-
feature = f"{parent.name}.{obj.name}"
186-
if (
187-
feature not in deprecated.qualified
188-
and parent.name not in deprecated.top_level
189-
):
204+
try:
205+
for br in griffe.find_breaking_changes(old, new):
206+
obj = getattr(br, "obj", None)
207+
if not getattr(obj, "is_public", True):
208+
continue
209+
210+
# Skip ATTRIBUTE_CHANGED_VALUE when it's just Field metadata changes
211+
# (description, title, examples, etc.) - these don't affect runtime
212+
if br.kind == BreakageKind.ATTRIBUTE_CHANGED_VALUE:
213+
old_value = getattr(br, "old_value", None)
214+
new_value = getattr(br, "new_value", None)
215+
if _is_field_metadata_only_change(old_value, new_value):
216+
print(
217+
f"::notice title={title}::Ignoring Field metadata-only "
218+
f"change (non-breaking): {obj.name if obj else 'unknown'}"
219+
)
220+
continue
221+
222+
print(br.explain(style=ExplanationStyle.GITHUB))
223+
breakages.append(br)
224+
225+
if br.kind != BreakageKind.OBJECT_REMOVED:
226+
continue
227+
228+
parent = getattr(obj, "parent", None)
229+
if getattr(parent, "kind", None) != Kind.CLASS:
230+
continue
231+
232+
feature = f"{parent.name}.{obj.name}"
233+
if (
234+
feature not in deprecated.qualified
235+
and parent.name not in deprecated.top_level
236+
):
237+
print(
238+
f"::error title={title}::Removed '{feature}' without prior "
239+
"deprecation. Mark it with @deprecated(...) or "
240+
f"warn_deprecated('{feature}', ...) for at least one release "
241+
"before removing."
242+
)
243+
undeprecated_removals += 1
244+
except AliasResolutionError as e:
245+
if isinstance(old, Alias) or isinstance(new, Alias):
246+
old_target = old.target_path if isinstance(old, Alias) else None
247+
new_target = new.target_path if isinstance(new, Alias) else None
248+
if old_target != new_target:
249+
name = getattr(old, "name", None) or getattr(
250+
new, "name", "<unknown>"
251+
)
252+
print(
253+
f"::warning title={title}::Alias target changed for '{name}': "
254+
f"{old_target!r} -> {new_target!r}"
255+
)
256+
breakages.append(
257+
{
258+
"kind": "ALIAS_TARGET_CHANGED",
259+
"name": name,
260+
"old": old_target,
261+
"new": new_target,
262+
}
263+
)
264+
else:
190265
print(
191-
f"::error title={title}::Removed '{feature}' without prior "
192-
"deprecation. Mark it with @deprecated(...) or "
193-
f"warn_deprecated('{feature}', ...) for at least one release "
194-
"before removing."
266+
f"::notice title={title}::Skipping symbol comparison due to "
267+
f"unresolved alias: {e}"
195268
)
196-
undeprecated_removals += 1
269+
except Exception as e:
270+
print(f"::warning title={title}::Failed to compute breakages: {e}")
197271

198272
return breakages, undeprecated_removals
199273

@@ -247,7 +321,7 @@ def _check_version_bump(prev: str, new_version: str, total_breaks: int) -> int:
247321
0 if policy satisfied, 1 if not
248322
"""
249323
if total_breaks == 0:
250-
print("No SDK breaking changes detected")
324+
print("No breaking changes detected")
251325
return 0
252326

253327
parsed_prev = _parse_version(prev)
@@ -260,14 +334,14 @@ def _check_version_bump(prev: str, new_version: str, total_breaks: int) -> int:
260334

261335
if not ok:
262336
print(
263-
f"::error title=SDK SemVer::Breaking changes detected ({total_breaks}); "
337+
f"::error title=SemVer::Breaking changes detected ({total_breaks}); "
264338
f"require at least minor version bump from "
265339
f"{parsed_prev.major}.{parsed_prev.minor}.x, but new is {new_version}"
266340
)
267341
return 1
268342

269343
print(
270-
f"SDK breaking changes detected ({total_breaks}) and version bump policy "
344+
f"Breaking changes detected ({total_breaks}) and version bump policy "
271345
f"satisfied ({prev} -> {new_version})"
272346
)
273347
return 0
@@ -452,9 +526,7 @@ def _get_source_root(griffe_root: object) -> Path | None:
452526
return None
453527

454528

455-
def _compute_breakages(
456-
old_root, new_root, cfg: PackageConfig, include: list[str]
457-
) -> tuple[int, int]:
529+
def _compute_breakages(old_root, new_root, cfg: PackageConfig) -> tuple[int, int]:
458530
"""Detect breaking changes between old and new package versions.
459531
460532
Returns:
@@ -468,79 +540,66 @@ def _compute_breakages(
468540
total_breaks = 0
469541
undeprecated_removals = 0
470542

471-
deprecated = DeprecatedSymbols()
543+
source_root = _get_source_root(old_root)
544+
deprecated = (
545+
_find_deprecated_symbols(source_root) if source_root else DeprecatedSymbols()
546+
)
472547

473548
try:
474549
old_mod = _resolve_griffe_object(old_root, pkg, root_package=pkg)
475550
new_mod = _resolve_griffe_object(new_root, pkg, root_package=pkg)
476-
old_exports = _extract_exported_names(old_mod)
477-
new_exports = _extract_exported_names(new_mod)
478-
479-
removed = sorted(old_exports - new_exports)
551+
except Exception as e:
552+
raise RuntimeError(f"Failed to resolve root module '{pkg}'") from e
480553

481-
source_root = _get_source_root(old_root)
482-
deprecated = (
483-
_find_deprecated_symbols(source_root)
484-
if source_root
485-
else DeprecatedSymbols()
554+
new_exports = _extract_exported_names(new_mod)
555+
try:
556+
old_exports = _extract_exported_names(old_mod)
557+
except ValueError as e:
558+
# The API breakage check relies on a curated public surface defined via
559+
# __all__. If the previous release didn't define (or couldn't statically
560+
# evaluate) __all__, we can't compute meaningful breakages.
561+
#
562+
# In this situation, skip rather than failing the entire workflow.
563+
print(
564+
f"::notice title={title}::Skipping breakage check; previous release "
565+
f"has no statically-evaluable {pkg}.__all__: {e}"
486566
)
567+
return 0, 0
487568

488-
# Check deprecation-before-removal policy (exports)
489-
if removed:
490-
for name in removed:
491-
total_breaks += 1 # every removal is a structural break
492-
if name not in deprecated.top_level:
493-
print(
494-
f"::error title={title}::Removed '{name}' from "
495-
f"{pkg}.__all__ without prior deprecation. "
496-
"Mark it with @deprecated or warn_deprecated() "
497-
"for at least one release before removing."
498-
)
499-
undeprecated_removals += 1
500-
else:
501-
print(
502-
f"::notice title={title}::Removed previously-"
503-
f"deprecated symbol '{name}' from "
504-
f"{pkg}.__all__"
505-
)
569+
removed = sorted(old_exports - new_exports)
506570

507-
common = sorted(old_exports & new_exports)
508-
pairs: list[tuple[object, object]] = []
509-
for name in common:
510-
try:
511-
pairs.append((old_mod[name], new_mod[name]))
512-
except Exception as e:
513-
print(f"::warning title={title}::Unable to resolve symbol {name}: {e}")
514-
515-
breakages, undeprecated_members = _collect_breakages_pairs(
516-
pairs,
517-
deprecated=deprecated,
518-
title=title,
519-
)
520-
total_breaks += len(breakages)
521-
undeprecated_removals += undeprecated_members
522-
except Exception as e:
523-
print(f"::warning title={title}::Failed to process top-level exports: {e}")
571+
# Check deprecation-before-removal policy (exports)
572+
for name in removed:
573+
total_breaks += 1 # every removal is a structural break
574+
if name not in deprecated.top_level:
575+
print(
576+
f"::error title={title}::Removed '{name}' from "
577+
f"{pkg}.__all__ without prior deprecation. "
578+
"Mark it with @deprecated or warn_deprecated() "
579+
"for at least one release before removing."
580+
)
581+
undeprecated_removals += 1
582+
else:
583+
print(
584+
f"::notice title={title}::Removed previously-deprecated symbol "
585+
f"'{name}' from {pkg}.__all__"
586+
)
524587

525-
extra_pairs: list[tuple[object, object]] = []
526-
for path in include:
527-
if path == pkg:
528-
continue
588+
common = sorted(old_exports & new_exports)
589+
pairs: list[tuple[object, object]] = []
590+
for name in common:
529591
try:
530-
old_obj = _resolve_griffe_object(old_root, path, root_package=pkg)
531-
new_obj = _resolve_griffe_object(new_root, path, root_package=pkg)
532-
extra_pairs.append((old_obj, new_obj))
592+
pairs.append((old_mod[name], new_mod[name]))
533593
except Exception as e:
534-
print(f"::warning title={title}::Path {path} not found: {e}")
594+
print(f"::warning title={title}::Unable to resolve symbol {name}: {e}")
535595

536-
if extra_pairs:
537-
breakages, undeprecated_members = _collect_breakages_pairs(
538-
extra_pairs,
539-
deprecated=deprecated,
540-
title=title,
541-
)
542-
total_breaks += len(breakages)
543-
undeprecated_removals += undeprecated_members
596+
breakages, undeprecated_members = _collect_breakages_pairs(
597+
pairs,
598+
deprecated=deprecated,
599+
title=title,
600+
)
601+
total_breaks += len(breakages)
602+
undeprecated_removals += undeprecated_members
544603

545604
return total_breaks, undeprecated_removals
546605

@@ -550,10 +609,6 @@ def _check_package(griffe_module, repo_root: str, cfg: PackageConfig) -> int:
550609
pyproj = os.path.join(repo_root, cfg.source_dir, "pyproject.toml")
551610
new_version = read_version_from_pyproject(pyproj)
552611

553-
include_env = f"{cfg.package.upper().replace('.', '_')}_INCLUDE_PATHS"
554-
include = os.environ.get(include_env, cfg.package).split(",")
555-
include = [p.strip() for p in include if p.strip()]
556-
557612
title = f"{cfg.distribution} API"
558613
prev = get_prev_pypi_version(cfg.distribution, new_version)
559614
if not prev:
@@ -573,7 +628,11 @@ def _check_package(griffe_module, repo_root: str, cfg: PackageConfig) -> int:
573628
if not old_root:
574629
return 1
575630

576-
total_breaks, undeprecated = _compute_breakages(old_root, new_root, cfg, include)
631+
try:
632+
total_breaks, undeprecated = _compute_breakages(old_root, new_root, cfg)
633+
except Exception as e:
634+
print(f"::error title={title}::Failed to compute breakages: {e}")
635+
return 1
577636

578637
if undeprecated:
579638
print(

0 commit comments

Comments
 (0)