Provider SDK Extensions Guide

Copy Markdown View Source

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:

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

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:

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. Amp derivation forwards both :transport_timeout_ms as :stream_timeout_ms and the independent :transport_headless_timeout_ms orphan-reap bound. 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:

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:

alias ASM.Extensions.ProviderSDK.Claude

asm_opts = [
  provider: :claude,
  cwd: File.cwd!(),
  execution_environment: [permission_mode: :plan],
  model: "sonnet",
  reasoning_effort: :high,
  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:

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:

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:

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, :effort, :max_turns, and :timeout_ms must stay in ASM config and are rejected from native_overrides; callers select effort with ASM's :reasoning_effort
  • 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.