Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- BREAKING: built-in adapters no longer pin library-owned default model names.
Model selection now follows task field > legacy metadata > `default_model=`
constructor override > provider-native configuration. Provider-native calls
omit the model kwarg; results expose `model_source` and only report a model
value when known. Existing constructor overrides remain supported.
- Pydantic structured-output parsing now requests strict validation, so values
such as `"42"` no longer coerce into integer fields.
- BREAKING: task and value-object constructors now reject blank identities,
Expand Down Expand Up @@ -62,6 +67,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Configured model allow-lists, Antigravity MCP name syntax, disjoint
allow/deny lists, and Antigravity's legacy reasoning-effort alias are rejected
during static preflight instead of surfacing later or being silently ignored.
- A configured model allow-list now fails closed when model selection is
provider-native and therefore cannot be verified locally.

## 0.4.0 - 2026-07-02

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ a field (for example only Claude maps `budget_usd`; Codex and Antigravity reject
it with a typed `UnsupportedTaskInputError`) the adapter raises rather than
silently dropping it.

Model selection follows one explicit precedence chain: `AgentTask.model`, then
legacy `metadata["model"]`, then an adapter's `default_model=` constructor
override, then the provider's native configuration/default. Adapters omit the
vendor model option at the final step rather than pinning a library-owned model.
Results record `metadata["model_source"]`; `metadata["model"]` appears only when
the kit knows the selected value.

Call `validate_task(runtime, task)` (or `kit.validate_task("codex", task)`) to
inspect every statically detectable incompatibility before dispatch. The
returned `TaskSupportReport` is side-effect-free and lists source fields such as
Expand Down
7 changes: 7 additions & 0 deletions docs/api-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ import the names from the top-level package instead.
without the extension maps a missing package to `NOT_READY` and present
package to `INDETERMINATE`. `READY_TO_ATTEMPT` establishes only that known
setup signals are present; it is not a guarantee of future execution.
- **Providers own the default model.** With no task, legacy metadata, or
`default_model=` override, built-in adapters omit the SDK model option and let
the provider's supported configuration select it. The effective precedence is
task field, metadata alias, constructor override, provider-native. Result
metadata always records `model_source` and records `model` only when known.
A configured `supported_models` allow-list requires an explicit/verifiable
selection and fails closed on provider-native selection.
- **`AgentKit` is sugar, not a second API.** The hub assembles the same frozen
`AgentTask` and returns the same `AgentResult` the runtimes produce
(`ParsedResult` is a runtime-identical subclass adding only the typed
Expand Down
1 change: 1 addition & 0 deletions docs/capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
|------------|------------------|------------------|------------------------|
| Optional extra | `claude` | `codex` | `antigravity` |
| Core import without extra | Yes | Yes | Yes |
| Model selection | Provider-native unless task/metadata/constructor overrides it | Provider-native unless task/metadata/constructor overrides it | Provider-native unless task/metadata/constructor overrides it |
| Working directory | Yes | Yes | Yes |
| Session resume | Yes | Yes | Yes |
| Structured output | Native `output_format` when available | Native output schema / JSON parse fallback | Native response schema / JSON parse fallback |
Expand Down
9 changes: 8 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ to the exact missing extra.

All three adapters map the task's system prompt (Claude `system_prompt`, Codex
`developer_instructions`, Antigravity `system_instructions`) and the `model`
field (falling back to the `metadata` aliases of the same names).
field. Model precedence is `AgentTask.model` > legacy `metadata["model"]` > the
adapter's `default_model=` constructor override > provider-native selection. At
the final step the adapter omits the SDK model option; it does not impose a
library-owned model that can go stale. Results always include
`metadata["model_source"]` (`task`, `metadata`, `constructor`, or
`provider-native`) and include `metadata["model"]` only when the selected value
is known. When `supported_models=` is configured, provider-native selection is
rejected as unverifiable until the caller chooses an explicit model.
`reasoning_effort` maps to the Claude and Codex `effort` options; Antigravity
has no reasoning-effort control and rejects the first-class field with a typed
error. Its legacy `metadata["reasoning_effort"]` alias is rejected too, rather
Expand Down
76 changes: 65 additions & 11 deletions src/agent_runtime_kit/adapters/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import inspect
import os
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from importlib import metadata
from math import isfinite
from typing import Any
from typing import Any, Literal

from agent_runtime_kit._errors import UnsupportedTaskInputError
from agent_runtime_kit._schema import (
Expand All @@ -26,6 +27,16 @@
)
from agent_runtime_kit.compatibility import compatibility_for

ModelSource = Literal["task", "metadata", "constructor", "provider-native"]


@dataclass(frozen=True)
class ModelSelection:
"""Effective model value and the layer that selected it."""

value: str | None
source: ModelSource


def package_availability(kind: AgentRuntimeKind) -> RuntimeAvailability:
"""Return installed-distribution availability without resolving a module."""
Expand Down Expand Up @@ -58,26 +69,69 @@ def package_version(package_name: str) -> str | None:
return None


def select_model(task: AgentTask, default_model: str | None) -> ModelSelection:
"""Resolve model precedence without inventing a provider default."""

if task.model is not None:
return ModelSelection(task.model, "task")
metadata_model = metadata_str(task.metadata, "model")
if metadata_model is not None:
return ModelSelection(metadata_model, "metadata")
if default_model is not None:
return ModelSelection(default_model, "constructor")
return ModelSelection(None, "provider-native")


def validate_model_configuration(
default_model: str | None,
supported_models: tuple[str, ...] | None,
) -> tuple[str, ...] | None:
"""Validate and freeze adapter-level model overrides and allow-lists."""

if default_model is not None and (
not isinstance(default_model, str) or not default_model.strip()
):
raise ValueError("default_model must be a non-empty string or None")
if supported_models is None:
return None
if isinstance(supported_models, (str, bytes)):
raise ValueError("supported_models must be a sequence, not a scalar string")
values = tuple(supported_models)
if any(not isinstance(value, str) or not value.strip() for value in values):
raise ValueError("supported_models must contain only non-empty strings")
if len(values) != len(set(values)):
raise ValueError("supported_models must not contain duplicates")
return values


def model_support_issue(
*,
task: AgentTask,
model: str,
selection: ModelSelection,
supported_models: tuple[str, ...] | None,
) -> TaskSupportIssue | None:
"""Report a configured model allow-list mismatch at the source field."""

if supported_models is None or model in supported_models:
if supported_models is None:
return None
supported = ", ".join(supported_models) or "(none)"
if task.model is not None:
field = "model"
elif metadata_str(task.metadata, "model") is not None:
field = "metadata.model"
else:
field = "model"
if selection.value is None:
return TaskSupportIssue(
"model",
"runtime has a configured model allow-list, but provider-native "
"selection cannot be verified; select an explicit model from: "
f"{supported}",
)
if selection.value in supported_models:
return None
field = {
"task": "model",
"metadata": "metadata.model",
"constructor": "default_model",
"provider-native": "model",
}[selection.source]
return TaskSupportIssue(
field,
f"model {model!r} is not supported by this runtime; supported: {supported}",
f"model {selection.value!r} is not supported by this runtime; supported: {supported}",
)


Expand Down
38 changes: 24 additions & 14 deletions src/agent_runtime_kit/adapters/antigravity.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@
empty_completion_error,
filter_supported_kwargs,
fingerprint_item,
metadata_str,
model_support_issue,
optional_int,
optional_str,
output_schema_from,
package_availability,
resolve_structured_output,
select_model,
validate_model_configuration,
)
from agent_runtime_kit.events import (
output_delta_event,
Expand Down Expand Up @@ -78,7 +79,7 @@ class AntigravityAgentRuntime:
def __init__(
self,
*,
default_model: str = "gemini-3.5-flash",
default_model: str | None = None,
supported_models: tuple[str, ...] | None = None,
api_key: str | None = None,
vertex: bool | None = None,
Expand All @@ -92,7 +93,9 @@ def __init__(
reuse_process: bool = False,
) -> None:
self._default_model = default_model
self._supported_models = supported_models
self._supported_models = validate_model_configuration(
default_model, supported_models
)
self._api_key = api_key
self._vertex = vertex
self._project = project
Expand Down Expand Up @@ -197,9 +200,9 @@ def validate_task(self, task: AgentTask) -> TaskSupportReport:

report = _validate_declared_task_support(self.kind, self.capabilities, task)
issues = list(report.issues)
selection = select_model(task, self._default_model)
model_issue = model_support_issue(
task=task,
model=self._model(task),
selection=selection,
supported_models=self._supported_models,
)
if model_issue is not None:
Expand Down Expand Up @@ -229,7 +232,8 @@ async def run(self, task: AgentTask) -> AgentResult:
await safe_emit(task, task_started_event(task, self.kind))
try:
require_task_support(self.validate_task(task))
model = self._model(task)
selection = select_model(task, self._default_model)
model = selection.value
sdk = self._load_sdk()
# Resolve auth off the event loop: ADC discovery can call
# google.auth.default(), which reads files and may hit the GCE metadata
Expand All @@ -243,7 +247,12 @@ async def run(self, task: AgentTask) -> AgentResult:
)
config, dropped = self._build_config(task, model=model, auth=auth, sdk=sdk)
result = await self._invoke(
task, config=config, sdk=sdk, model=model, dropped_options=dropped
task,
config=config,
sdk=sdk,
model=model,
model_source=selection.source,
dropped_options=dropped,
)
except Exception as exc:
await safe_emit(task, task_failed_event(task, self.kind, error=str(exc)))
Expand Down Expand Up @@ -307,7 +316,7 @@ def _build_config(
self,
task: AgentTask,
*,
model: str,
model: str | None,
auth: _AntigravityAuthConfig,
sdk: _AntigravitySDK,
) -> tuple[Any, list[str]]:
Expand All @@ -321,7 +330,6 @@ def _build_config(
capabilities, policies = _capability_policy(self.kind, task, sdk)
schema = output_schema_from(task.output_schema, task.metadata)
config_kwargs: dict[str, Any] = {
"model": model,
"api_key": auth.api_key,
"vertex": auth.vertex,
"project": auth.project,
Expand All @@ -343,6 +351,8 @@ def _build_config(
for server in task.mcp_servers
],
}
if model is not None:
config_kwargs["model"] = model
# Tolerate vendor option drift like Claude/Codex: drop kwargs the installed
# LocalAgentConfig no longer accepts (instead of a TypeError) and record
# them — except the tool posture (and workspace scoping when requested),
Expand All @@ -361,7 +371,8 @@ async def _invoke(
*,
config: Any,
sdk: _AntigravitySDK,
model: str,
model: str | None,
model_source: str,
dropped_options: list[str] | None = None,
) -> AgentResult:
text_parts: list[str] = []
Expand Down Expand Up @@ -426,10 +437,12 @@ async def _invoke(
self._process_reuse_metadata(process_reused) if self._reuse_process else None
)
metadata: dict[str, Any] = {
"model": model,
"model_source": model_source,
"sdk": "google_antigravity",
**dict(process_metadata or {}),
}
if model is not None:
metadata["model"] = model
if dropped_options:
metadata["dropped_options"] = list(dropped_options)

Expand Down Expand Up @@ -661,9 +674,6 @@ def _auth_config(self) -> _AntigravityAuthConfig:
return _AntigravityAuthConfig(source="none")
return self._vertex_auth_config()

def _model(self, task: AgentTask) -> str:
return task.model or metadata_str(task.metadata, "model") or self._default_model

def _runtime_dir(self, name: str) -> Path:
base = self._data_dir or _default_data_dir()
path = base / name
Expand Down
Loading