Skip to main content

Module 33: Session Config Options

Expose per-session settings (a model picker, a mode, and a capability-gated boolean) as session config options, change them with session/set_config_option, and push an agent-initiated change with a config_option_update. One agent, written twice: builder API and annotations.

What You’ll Learn

  • Building a model picker with SessionConfigSelect.model(...), and a categorized select with SessionConfigSelect.builder()
  • Offering a SessionConfigBoolean only to clients that advertised support for it
  • Returning the full option list from session/new and every subsequent change, never a delta
  • Sending ConfigOptionUpdate only when the agent changes a setting itself
  • Rejecting a bad request with -32602, since the SDK validates nothing about a set_config_option call
  • Reading the id and value directly with @ConfigId/@ConfigValue, typed parameters instead of the raw request
  • Keeping legacy session/set_mode in step with a category: "mode" config option
  • Dispatching option types with instanceof and an UnknownSessionConfigOption branch

The Code

Declaring the options

Three options, one state object shared by both agent variants below. The model picker uses the dedicated factory; the mode select and the boolean use the builders:
The SDK does not filter the boolean option for you: the agent checks the client’s advertised capabilities itself, once, at session/new.

Annotated agent

Any handler, not only @Prompt, may declare NegotiatedCapabilities and AcpSyncAgent/AcpAsyncAgent parameters, resolved by type, for the connection the request arrived on. That’s why @NewSession reads supportsBooleanConfigOptions() straight from a parameter here, with no need to capture the built agent. @ConfigId String and @ConfigValue (typed by the parameter: String for a select’s value, boolean/Boolean for a boolean, Object for either, as above) replace pulling configId() and value() out of the raw request. The SDK still doesn’t check that the session actually offered that id or value, only that the value is the right kind; whether it’s a value the session offers is the method’s own job, the same as with the raw request.

The same agent, with the builder API

As of 0.80.0, a builder handler other than the prompt handler can take the agent as a second parameter, (req, self) -> ..., so session/new reads the client’s capabilities straight off self.getClientCapabilities(), with no AtomicReference needed to reach the agent build() is about to return.

Rejecting a bad request

The spec defines no error for an invalid set_config_option; this agent answers -32602:
req.value() is an Object: a String for a select, a Boolean for a boolean, so the handler checks its type before trusting it, the same as the id.

Client: read, change, and watch for agent-initiated changes

Dispatch option types with instanceof and keep a branch for a type a newer agent might send:
An agent-initiated change (the demo’s rate-limited preview model falling back) arrives as a ConfigOptionUpdate session update, handled before prompt() returns:
Setting an option the client was never offered (a boolean to a client that didn’t advertise support, or a model id that isn’t in the list) is rejected with -32602, received as an AcpError. A client that predates config options can still work through the legacy session/set_mode: the agent keeps both representations of the same state in sync.

Source Code

View on GitHub

Running the Example

The demo runs three passes: the builder agent and the annotated agent, both advertising boolean config options, then the builder agent again without that capability, where the verbose option is never offered and setting it is rejected, and a legacy session/set_mode call is shown keeping the same state.

Next Module

Module 34: Extension Methods: add your own _-prefixed requests and notifications to ACP.