Current section

Files

Jump to
agent_session_manager guides provider-sdk-extensions.md
Raw

guides/provider-sdk-extensions.md

# Provider SDK Extensions Guide
Phase 3 keeps an explicit optional layer for provider-native surfaces above
ASM's normalized kernel.
## Kernel Versus Extension Split
Normalized kernel surfaces stay where they already belong:
- `ASM`
- `ASM.Stream`
- `ASM.Result`
- `ASM.ProviderRegistry`
Those APIs continue to own:
- provider selection
- lane selection
- normalized event/result projection
- session/run orchestration
Provider-native extension discovery now lives under
`ASM.Extensions.ProviderSDK`.
Schema ownership follows the same split:
- ASM owns orchestration, provider-option, event, and remote-node envelopes.
- provider-native runtime and protocol schemas stay local to the owning SDK
repo.
- `NimbleOptions` can still front the public ASM keyword API, but `Zoi` is the
canonical schema layer underneath new dynamic boundaries.
## Dependency Model
ASM keeps `cli_subprocess_core` required and all provider SDK packages
optional.
- depending only on `:agent_session_manager` gives you the common ASM surface
- adding any provider SDK activates the matching SDK lane and, when one is
registered, the matching provider-native namespace
- declaring the optional dependency is the only client-app activation step;
ASM performs discovery and activation automatically
ASM handles those optional dependencies through runtime-checked module loading
and explicit extension/backend boundaries. The kernel never relies on
compile-time warning suppression to reference provider SDK packages.
## Why This Is Separate
`ASM.ProviderRegistry` keeps normalized discovery on its existing surfaces:
- `provider_info/1`: `available_lanes`, `core_capabilities`, `sdk_capabilities`
- `lane_info/2`: `preferred_lane`, `backend`
- `resolve/2`: effective `lane`, `backend`
`ASM.Extensions.ProviderSDK` reports provider-native extension facts:
- explicit optional namespace modules
- provider-native capability inventory
- whether the backing SDK package is loadable locally
That keeps lane discovery and provider-native surface discovery from collapsing
into one API.
## Discovery API
```elixir
alias ASM.Extensions.ProviderSDK
ProviderSDK.extensions()
ProviderSDK.available_extensions()
{:ok, claude_extension} = ProviderSDK.extension(:claude)
{:ok, active_claude_extensions} = ProviderSDK.available_provider_extensions(:claude)
{:ok, codex_extensions} = ProviderSDK.provider_extensions(:codex)
{:ok, codex_native_caps} = ProviderSDK.provider_capabilities(:codex)
report = ProviderSDK.capability_report()
```
Current built-in namespaces:
- `ASM.Extensions.ProviderSDK.Amp`
- `ASM.Extensions.ProviderSDK.Claude`
- `ASM.Extensions.ProviderSDK.Codex`
- `ASM.Extensions.ProviderSDK.Cursor`
These root modules are the namespace anchors for optional provider-native
helpers.
Antigravity is a first-party provider with core and optional SDK runtime lanes,
but it does not currently register an
`ASM.Extensions.ProviderSDK.Antigravity` namespace. Direct SDK-lane execution
uses `AntigravityCliSdk.Runtime.CLI`, and direct SDK ownership remains in the
`antigravity_cli_sdk` package.
`ASM.Extensions.ProviderSDK.Cursor` is backed by `cursor_cli_sdk` when that
dependency is present. Cursor also keeps the common core lane through
`cli_subprocess_core`.
Activation-aware discovery follows a separate rule:
- `extensions/0` is the static registered native-extension catalog
- `provider_extensions/1` is the static native-extension catalog for one
provider
- `available_extensions/0` reports which of those namespaces are active for the
currently installed optional deps
- `available_provider_extensions/1` reports the active native-extension subset
for one provider
- `provider_report/1` and `capability_report/0` always include all ASM
providers, including Cursor, Amp, and Antigravity, and show whether
each provider SDK runtime is available plus any active native namespace
inventory
- `registered_namespaces` and `registered_extensions` keep the static catalog
visible even when a provider currently composes only through the common
surface
- Cursor and Amp start with strict derivation helpers. They still have
explicit namespaces so Cursor mode/MCP/plugin settings and Amp
permissions/MCP/skills have one provider-native home.
Antigravity has no derivation helper in this namespace layer yet; its SDK
lane is available through the runtime kit when installed.
The first argument to each `derive_options/2` helper is checked with strict common ASM preflight. Provider-native
keys in that map are rejected. Native provider data must be passed explicitly in
`native_overrides` or through the provider SDK directly.
Claude now exposes an explicit bridge into the SDK-local control family:
- `ASM.Extensions.ProviderSDK.Claude.derive_options/2`
- `ASM.Extensions.ProviderSDK.Claude.sdk_options/2`
- `ASM.Extensions.ProviderSDK.Claude.sdk_options_for_session/3`
- `ASM.Extensions.ProviderSDK.Claude.start_client/3`
- `ASM.Extensions.ProviderSDK.Claude.start_client_for_session/4`
Those helpers do not redefine Claude control semantics. They only:
- derive `ClaudeAgentSDK.Options` from ASM-style config or session defaults
- preserve ASM execution-surface placement on the resulting SDK options
- keep Claude-native options in a separate `native_overrides` bag
- start `ClaudeAgentSDK.Client` when callers explicitly opt into the SDK-local
control family
Example:
```elixir
alias ASM.Extensions.ProviderSDK.Claude
asm_opts = [
provider: :claude,
cwd: File.cwd!(),
execution_environment: [permission_mode: :plan],
model: "sonnet",
execution_surface: [
surface_kind: :ssh_exec,
transport_options: [destination: "buildbox-a", port: 2222]
]
]
native_overrides = [
enable_file_checkpointing: true,
thinking: %{type: :adaptive}
]
{:ok, client} =
Claude.start_client(
asm_opts,
native_overrides,
control_request_timeout_ms: 5_000
)
:ok = ClaudeAgentSDK.Client.set_permission_mode(client, :plan)
```
The normalized ASM APIs stay unchanged. Only the optional extension crosses
into the Claude-native client surface.
Codex now exposes a similarly narrow bridge into the SDK-local app-server
entry path:
- `ASM.Extensions.ProviderSDK.Codex.derive_options/2`
- `ASM.Extensions.ProviderSDK.Codex.codex_options/2`
- `ASM.Extensions.ProviderSDK.Codex.codex_options_for_session/3`
- `ASM.Extensions.ProviderSDK.Codex.thread_options/2`
- `ASM.Extensions.ProviderSDK.Codex.thread_options_for_session/3`
- `ASM.Extensions.ProviderSDK.Codex.connect_app_server/3`
- `ASM.Extensions.ProviderSDK.Codex.connect_app_server_for_session/4`
Those helpers do not re-model app-server, MCP, realtime, or voice as ASM
kernel APIs. They only:
- derive `Codex.Options` from ASM-style config or session defaults
- derive `Codex.Thread.Options` from ASM-style config or session defaults
- preserve ASM execution-surface placement on `Codex.Options`
- keep Codex-native global or thread-only fields in explicit override bags
- start `Codex.AppServer` when callers explicitly opt into the SDK-local
app-server family
The normalized ASM backend has one promoted Codex app-server path: host dynamic
tools. When a Codex run requests `app_server: true`, `host_tools: [...]`, or
`dynamic_tools: [...]`, `ASM.ProviderBackend.SDK` starts app-server, renders the
tool specs, handles `DynamicToolCallRequested`, and emits ASM host-tool events.
Other Codex app-server APIs still belong to `codex_sdk` or this explicit
extension seam.
Example:
```elixir
alias ASM.Extensions.ProviderSDK.Codex
alias Codex, as: CodexSDK
{:ok, conn} =
Codex.connect_app_server(
[
provider: :codex,
model: "gpt-5.6-sol",
reasoning_effort: :max,
execution_environment: [permission_mode: :default],
execution_surface: [
surface_kind: :ssh_exec,
transport_options: [destination: "codex-host-1"],
lease_ref: "lease-42"
]
],
[model_personality: :pragmatic],
experimental_api: true
)
{:ok, thread_opts} =
Codex.thread_options(
[
provider: :codex,
cwd: "/repo",
execution_environment: [permission_mode: :default],
approval_timeout_ms: 45_000,
output_schema: %{"type" => "object"}
],
transport: {:app_server, conn},
personality: :pragmatic
)
{:ok, codex_opts} =
Codex.codex_options(
[provider: :codex, model: "gpt-5.6-sol"],
model_personality: :pragmatic
)
{:ok, thread} = CodexSDK.start_thread(codex_opts, thread_opts)
```
ASM intentionally does not normalize Codex `:auto` onto `Codex.Thread.Options`
because the current Codex workspace-write auto-edit path dirties repo roots
with a `.codex` artifact. Keep ASM's Codex bridge on `:default` or `:bypass`,
or use `codex_sdk` directly when you explicitly need provider-native
workspace-write behavior.
Cursor exposes the same strict derivation shape for `cursor_cli_sdk`:
```elixir
alias ASM.Extensions.ProviderSDK.Cursor
asm_common = [
provider: :cursor,
model: "composer-2.5-fast",
cwd: File.cwd!(),
execution_surface: [surface_kind: :local_subprocess]
]
native_overrides = [
mode: :ask,
approve_mcps: true,
plugin_dirs: ["priv/cursor/plugins"]
]
{:ok, cursor_options} =
Cursor.derive_options(asm_common, native_overrides: native_overrides)
```
The resulting struct is `%CursorCliSdk.Options{}`. The SDK lane uses
`CursorCliSdk.Runtime.CLI`; the core lane remains
`CliSubprocessCore.ProviderProfiles.Cursor`.
Cursor-native settings such as `:mode`, `:sandbox`, `:approve_mcps`, worktree
controls, plugin dirs, and headers belong in `native_overrides`. ASM-derived
fields such as `:model`, `:cwd`, `:execution_surface`, `:timeout_ms`, and
launch auth placement stay in ASM config.
## Optional-Loading Rules
- discovery calls are always available from ASM
- each extension reports `sdk_available?`
- `sdk_available?` only means the backing SDK package is loadable locally
- `provider_capabilities/1` reports only active native capabilities for the
current dependency set
- richer provider-native APIs still live in the provider SDK repos
- ASM does not re-model those richer APIs in the kernel
- Antigravity reports as an ASM provider in `capability_report/0` with
`registered_namespaces: []` until a real native-extension namespace lands
- the Claude bridge keeps ASM config and Claude-native config in separate
arguments on purpose
- local `:core` and local `:sdk` lanes preserve the same normalized
`execution_surface` contract; `execution_mode: :remote_node`
remains a separate ASM-only rule
- ASM-derived fields such as `:cwd`, `:execution_environment`, `:model`,
`:max_turns`, and `:timeout_ms` must stay in ASM config and are rejected
from `native_overrides`
- the Codex bridge follows the same rule for ASM-derived fields such as
`:model`, `:reasoning_effort`, `:cwd`, `:approval_timeout_ms`, and
`:output_schema`
- the Codex bridge is intentionally useful at the app-server entry seam; ASM
only promotes host dynamic tools into the backend, while the broader
app-server, MCP, realtime, and voice APIs remain in `codex_sdk`
- the Cursor bridge derives `CursorCliSdk.Options`; Cursor-native settings
stay in `native_overrides`
## Current Native Capability Inventory
Claude namespace:
- `:control_client`
- `:control_protocol`
- `:hooks`
- `:permission_callbacks`
Codex namespace:
- `:app_server`
- `:mcp`
- `:realtime`
- `:voice`
Amp namespace:
- `:mcp`
- `:permissions`
- `:skills`
- `:threads`
Cursor namespace:
- `:mcp`
- `:plugins`
- `:worktrees`
Antigravity currently has no ProviderSDK namespace capability inventory. Its
core and SDK runtime capability surfaces are reported through
`ASM.ProviderFeatures` and `ASM.ProviderRegistry`, not through this native
extension catalog.
These are capability labels for discovery and documentation only in this
foundation slice. They are not new normalized kernel APIs.
## Session-Control Extensions
Provider SDK extensions may now publish optional callbacks and capability
markers for:
- session history listing
- exact or latest-session resume
- pause
- operator intervention
Those extensions are only valid when the provider runtime can back them with a
real native surface. ASM now keeps the provider-native recovery handles in
session/run state so a caller can choose exact resume before it falls back to
replaying prompts.