Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,87 @@
# Migration Guide: v2.7.x → v2.8.0

## Overview

v2.8.0 advances the pinned `github-copilot-sdk` from `1.0.7` to `1.0.14` (stable).
The move is **wire-compatible** — no public API, config key, env var, or CLI flag
changes, and no action is required on upgrade.

The 5 quarantined SDK imports (`CopilotClient`, `ModelCapabilitiesOverride`,
`ModelLimitsOverride`, `PermissionDecisionReject`, `PermissionRequestResult`) are
unchanged in name, location and shape. Nothing was removed or renamed across the
`1.0.8 → 1.0.14` span: `CopilotClient.__init__`, `CopilotClient.create_session`
and `CopilotSession.send` are each purely additive (verified by signature
set-difference — removed == none for all three).

Four SDK surface changes are absorbed by the provider:

- **New `Tool.is_terminal` field.** SDK 1.0.14's `copilot.tools.Tool` dataclass
gains a trailing `is_terminal: bool = False` field, which `copilot/client.py`
reads by attribute in **both** `create_session` (`:2570`) and `resume`
(`:3355`) as `if tool.is_terminal:`. The provider hands the SDK a duck-typed
`SDKToolWrapper`, so the wrapper now mirrors that field with a `False` default.
Note this read is a **truthiness** gate, not the `is not None` test used for
`defer` and `metadata`: `False` **omits** the `isTerminal` key from the wire
payload, keeping the tool-forwarding request byte-identical to 1.0.7. This is
the same shape as the v1.0.2 `defer` and v1.0.7 `metadata` field additions.
Without it, every tool-forwarding turn would raise `AttributeError`.
- **Two new mode-gated capabilities**, both pinned (MinimalMode MUST:17-18):
`custom_agents_local_only=True` and `enable_experimental_mode=False`. Each
mirrors the SDK's own empty-mode default. Under `mode="copilot-cli"` the
empty-mode helpers return `None`, so leaving either unset would hand the
setting to the bundled CLI — for `enable_experimental_mode` that means a CLI
upgrade could silently switch experimental behavior on underneath the provider.
- **`ReasoningEffort` Literal grows to 5 members.** `"max"` is now SDK-native; it
was a documented provider fallback-allowlist extra through 1.0.7. **The
provider's forwarded set is unchanged** — `"max"` simply moved from
provider-extra to SDK-native, and `"none"` remains the sole remaining extra.
- **27 new `SessionEventType` members**, all classified **DROP** (no kernel domain
mapping): the fusion pipeline (`session.fusion_*`, `assistant.fusion_phase_*`),
auto-tier advisories, `model.call_start`/`model.call_finished`,
`assistant.turn_retry`, `agent.interrupted`, four `session.*` state notices, two
MCP server-lifecycle events, `factory.run_*`, plus `prompt_cache_break`,
`tool_search.activated`, `sandbox.decision` and `ui.ephemeral_query`. See
`config/data/events.yaml` for the per-family reasoning.

`CopilotSession.send` also gains a keyword-only `source` parameter. The provider
does not pass it, so the SDK default preserves pre-1.0.14 wire behavior; the
signature pin was widened to record the true surface.

### Also in this release: dev-dependency floor `amplifier-core>=2.0.0`

**Contributors only — no runtime impact.** `amplifier-core` is injected by the
Amplifier runtime when the module is loaded; it is declared under
`[project.optional-dependencies] dev` for type hints and tests, not installed at
runtime by this package.

The floor moves `1.3.0 → 2.0.0` to match what the CLI actually ships (**2.0.1**).
The previous constraint was an unbounded `>=1.3.0`, so `uv.lock` resolved
**1.3.3** — meaning the test suite validated against 1.3.3 while production ran
2.0.1, a full major version apart, with nothing reporting the divergence. The
lock is regenerated to 2.0.1 and the suite is green against it (1586 passed).

**Action required:** none for users. Contributors with an existing dev
environment should re-sync (`uv sync --extra dev`) to pick up `amplifier-core`
2.x.

---

## What Changed: SDK requirement is now `github-copilot-sdk==1.0.14`

- **What:** The provider now pins `github-copilot-sdk==1.0.14` (was `==1.0.7`).
The import-time support floor is unchanged (`>=1.0.0`; pre-GA beta SDKs remain
unsupported). The `1.0.7 → 1.0.14` move is **non-breaking** for the provider's
wire behavior — `Tool.is_terminal` defaults to `False`, which reproduces the
1.0.7 tool payload exactly, and the two new MinimalMode pins mirror the SDK's
own empty-mode defaults.
- **Action required:** none. A clean `pip install` of `2.8.0` pulls `1.0.14`
automatically via the `pyproject.toml` pin; `amplifier provider install --force
github-copilot` reinstalls the pinned SDK for an existing environment.
- **Rollback:** if the previous SDK pin is required, pin
`amplifier-module-provider-github-copilot<2.8.0`.

---

# Migration Guide: v2.6.x → v2.7.0

## Overview
Expand Down
10 changes: 5 additions & 5 deletions amplifier_module_provider_github_copilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _check_sdk_version(version_str: str) -> None:
the technical symbol-availability minimum sits *below* this floor. The floor
is deliberately set higher, at GA, to drop beta-SDK support entirely.

The exact pyproject pin (``==1.0.7``) is enforced separately by
The exact pyproject pin (``==1.0.14``) is enforced separately by
``tests/_sdk_version_gate.py``; this guard only enforces the GA floor.

Extracted for testability — module-level code that runs under
Expand All @@ -75,8 +75,8 @@ def _check_sdk_version(version_str: str) -> None:
raise ImportError(
f"github-copilot-sdk=={version_str} is below the minimum supported "
"version (>=1.0.0 required; pre-GA beta SDKs are no longer supported). "
"Pinned target: ==1.0.7 (pyproject.toml). "
"Reinstall with: pip install 'github-copilot-sdk==1.0.7' "
"Pinned target: ==1.0.14 (pyproject.toml). "
"Reinstall with: pip install 'github-copilot-sdk==1.0.14' "
f"or: amplifier provider install --force {PROVIDER_ID}"
)

Expand Down Expand Up @@ -105,7 +105,7 @@ def _parse_sdk_version(version_str: str) -> _Version:
# SDK required; tests only run with SDK installed
raise ImportError(
"Required dependency 'github-copilot-sdk' is not installed. "
"Install with: pip install 'github-copilot-sdk==1.0.7'"
"Install with: pip install 'github-copilot-sdk==1.0.14'"
) from _e
# Contract: sdk-boundary:Membrane:MUST:5 — fail at import time on wrong version.
_check_sdk_version(_sdk_version)
Expand All @@ -127,7 +127,7 @@ def _parse_sdk_version(version_str: str) -> _Version:

# Contract: provider-protocol:public_api:MUST:1 — must match pyproject.toml [project].version
# Verified by tests/test_behaviors.py::TestPackageVersionConsistency
__version__ = "2.7.0"
__version__ = "2.8.0"

# Amplifier module metadata
__amplifier_module_type__ = "provider"
Expand Down
21 changes: 21 additions & 0 deletions amplifier_module_provider_github_copilot/config/_sdk_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ class MinimalModeConfig:
# sdk-boundary:MinimalMode SDK-introspection closure).
memory: dict[str, Any] = field(default_factory=lambda: {"enabled": False})

# MUST:17 — Confine custom-agent resolution to the local session; never let
# the SDK resolve agents from shared/remote sources. Added at v1.0.14 as a
# new mode-gated create_session kwarg with empty-mode default helper
# `_custom_agents_local_only_default` returning True ONLY when
# `mode == "empty"`. Our adapter ships `mode="copilot-cli"`, where the
# helper returns None and the bundled-CLI default would otherwise apply —
# so this explicit pin IS the wire shape. Complements MUST:5 (which only
# empties the custom_agents list). Value mirrors the SDK empty-mode default
# exactly (contract: sdk-boundary:MinimalMode SDK-introspection closure).
custom_agents_local_only: bool = True

# MUST:18 — Keep SDK experimental features off. Added at v1.0.14 as a new
# mode-gated create_session kwarg with empty-mode default helper
# `_enable_experimental_mode_default` returning False ONLY when
# `mode == "empty"`. Under `mode="copilot-cli"` the helper returns None and
# the bundled-CLI default applies, so without this pin a CLI upgrade could
# silently switch experimental behavior on underneath the provider — the
# wire shape must be owned here, not inherited. Value mirrors the SDK
# empty-mode default exactly.
enable_experimental_mode: bool = False


@dataclass(frozen=True)
class ToolCaptureConfig:
Expand Down
73 changes: 73 additions & 0 deletions amplifier_module_provider_github_copilot/config/data/events.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,79 @@ event_classifications:
- mcp.tools.list_changed
- mcp.resources.list_changed
- mcp.prompts.list_changed
# SDK v1.0.14 (bundled CLI 1.0.77) — 27 new SessionEventType enum members.
# All DROP, for the same standing reason as the v1.0.7 block above: the
# provider's domain surface is single-completion + tool-call + usage + error
# + content-deltas. None of the members below carry a kernel domain mapping,
# and each is a sibling of an already-dropped family.
#
# Fusion pipeline (SDK-internal multi-model routing; the provider selects
# models explicitly and never drives fusion — siblings of the already
# dropped session.auto_mode_resolved / auto_mode_switch.* analytics):
- session.fusion_route_started
- session.fusion_route_failed
- session.fusion_resolved
- session.fusion_completed
- assistant.fusion_phase_started
- assistant.fusion_phase_activity
- assistant.fusion_phase_completed
- assistant.fusion_phase_failed
#
# Auto-tier advisories (model/tier selection is Amplifier's; these are
# recommendation + failure notices, siblings of session.auto_mode_resolved):
- session.auto_tier_recommendation
- session.auto_tier_switch_failed
#
# Model call lifecycle — start/finish granularity for an upstream model
# call. Dropped for the SAME reason as model.call_failure above: the
# provider surfaces completion via its own turn accounting and errors via
# the session.error → ERROR bridge; bridging these would double-emit.
- model.call_start
- model.call_finished
#
# Turn/agent interruption + retry markers. Amplifier owns turn control and
# carries its own retry policy (retry_utils.py); these are SDK-internal
# progress telemetry, siblings of assistant.idle / assistant.turn_*.
- assistant.turn_retry
- agent.interrupted
#
# Session lifecycle/state notices with no kernel domain mapping — siblings
# of the session.context_changed / session.info / session.mode_changed /
# session.managed_settings_resolved entries already dropped above.
# (session.context_cleared in particular: Amplifier owns context
# construction, so an SDK-side clear notice carries no domain value.)
- session.context_cleared
- session.completion_receipt
- session.mode_notice_delivered
- session.managed_settings_enforced
#
# MCP server lifecycle — siblings of the already-dropped
# session.mcp_server_status_changed / session.mcp_servers_loaded.
# The provider pins mcp_servers to {} (MinimalMode MUST:3).
- session.mcp_server_removed
- session.mcp_server_needs_reconnect
#
# Factory run lifecycle (SDK-internal background run bookkeeping;
# siblings of the dropped session.background_tasks_changed).
- factory.run_started
- factory.run_updated
- factory.run_settled
#
# Remaining singletons:
# prompt_cache_break — upstream prompt-cache boundary marker; a
# performance/telemetry signal with no domain type.
# tool_search.activated — SDK lazy tool-search activation. The provider
# pre-loads all tools and pins defer=None, so this never fires in
# practice; classified explicitly rather than left to fall through.
# sandbox.decision — SDK-side sandbox policy decision. Amplifier
# registers a single deny-all hook and owns permissioning, so this is
# a sibling of the dropped permission.* family.
# ui.ephemeral_query — host UI prompt surface; the provider hosts no UI
# (sibling of the dropped elicitation.* / user_input.* families).
- prompt_cache_break
- tool_search.activated
- sandbox.decision
- ui.ephemeral_query

finish_reason_map:
# Values MUST be lowercase to match amplifier-core proto:
Expand Down
10 changes: 6 additions & 4 deletions amplifier_module_provider_github_copilot/request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@
# Provider reasoning-effort levels in ascending intensity order. Single source
# of truth shared by the request-path fallback allowlist below and the
# provider's reasoning_effort ConfigField choices (kept in sync by
# test_reasoning_effort_choices_match_levels). Superset of the v1.0.7 SDK
# ReasoningEffort Literal {low,medium,high,xhigh}: the live list_models endpoint
# additionally advertises "none" and "max", neither of which the v1.0.7 SDK
# Literal enumerates. Membership is pinned by the SDK-superset test in
# test_reasoning_effort_choices_match_levels). Superset of the v1.0.14 SDK
# ReasoningEffort Literal {low,medium,high,xhigh,max}: the live list_models
# endpoint additionally advertises "none", which the v1.0.14 SDK Literal does
# not enumerate. ("max" was also such an extra through v1.0.7 and became
# SDK-native at v1.0.14 — the provider's forwarded set is unchanged.)
# Membership is pinned by the SDK-superset test in
# tests/test_sdk_assumptions.py.
REASONING_EFFORT_LEVELS: tuple[str, ...] = ("none", "low", "medium", "high", "xhigh", "max")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ def _minimal_mode_session_config() -> dict[str, Any]:
# MUST:16 — v1.0.2 mode-gated 'memory' kwarg; pinned off (mirrors the
# SDK empty-mode default {"enabled": False}).
"memory": dict(minimal_mode.memory),
# MUST:17-18 — v1.0.14 mode-gated kwargs. See _sdk_protection.py for
# mode="copilot-cli" rationale (empty-mode default helpers do not fire).
"custom_agents_local_only": minimal_mode.custom_agents_local_only,
"enable_experimental_mode": minimal_mode.enable_experimental_mode,
}


Expand Down
14 changes: 14 additions & 0 deletions amplifier_module_provider_github_copilot/sdk_adapter/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,20 @@ class SDKToolWrapper:
# on every tool-forwarding turn.
# Contract: sdk-boundary:ToolForwarding:MUST:2
metadata: dict[str, Any] | None = None
# SDK v1.0.14 reads tool.is_terminal when building tool definitions in BOTH
# create_session and resume (copilot/client.py:2570 and :3355,
# `if tool.is_terminal: definition["isTerminal"] = True`, immediately after
# the `metadata` block). Mirrors the SDK's own copilot.tools.Tool.is_terminal
# field type. Note this is a TRUTHINESS gate, not `is not None` like `defer`
# and `metadata` above: False = the key is omitted from the wire payload,
# which is the exact pre-v1.0.14 behavior.
# A terminal tool ends the agent turn on a successful call. Amplifier owns
# turn control at the kernel layer and never delegates it to the SDK, so
# this stays False. Without this attribute the SDK raises AttributeError on
# every tool-forwarding turn (same shape as the v1.0.2 `defer` and v1.0.7
# `metadata` regressions).
# Contract: sdk-boundary:ToolForwarding:MUST:2
is_terminal: bool = False


def convert_tools_for_sdk(tools: list[Any]) -> list[SDKToolWrapper]:
Expand Down
Loading
Loading