CliSubprocessCore.ProviderProfile behaviour (CliSubprocessCore v0.7.0)

Copy Markdown View Source

Behaviour contract for built-in and external provider CLI profiles.

Summary

Types

What build_invocation/1 returns.

Event decode result returned by parser callbacks.

Normalized provider identifier.

Normalized invocation returned by a provider profile.

Parser state owned by an individual provider profile.

Releases resources an invocation needed on disk or in memory for the lifetime of the run. Idempotent, bounded, and safe to call from any process.

Functions

Whether a lane can be given more input after its run has started.

Validates that a module satisfies the provider profile contract.

Normalizes a build_invocation/1 return into {invocation, teardown}.

Returns the callbacks required by the provider profile contract.

Validates a normalized invocation returned by a provider profile.

Types

build_result()

@type build_result() ::
  {:ok, invocation()} | {:ok, invocation(), teardown()} | {:error, term()}

What build_invocation/1 returns.

A profile that materializes a resource for the run — a Codex output-schema file is the shipped example — returns it with a teardown the run's owner is responsible for calling. Owner death is covered independently by CliSubprocessCore.EphemeralFiles.

callback_spec()

@type callback_spec() ::
  {:id, 0}
  | {:capabilities, 0}
  | {:build_invocation, 1}
  | {:init_parser_state, 1}
  | {:decode_stdout, 2}
  | {:decode_stderr, 2}
  | {:handle_exit, 2}
  | {:transport_options, 1}

decode_result()

@type decode_result() :: {[CliSubprocessCore.Event.t()], parser_state()}

Event decode result returned by parser callbacks.

id()

@type id() :: atom()

Normalized provider identifier.

invocation()

@type invocation() :: CliSubprocessCore.Command.t()

Normalized invocation returned by a provider profile.

parser_state()

@type parser_state() :: term()

Parser state owned by an individual provider profile.

teardown()

@type teardown() :: (-> :ok)

Releases resources an invocation needed on disk or in memory for the lifetime of the run. Idempotent, bounded, and safe to call from any process.

Callbacks

build_invocation(keyword)

@callback build_invocation(keyword()) :: build_result()

capabilities()

@callback capabilities() :: [atom()]

decode_stderr(binary, parser_state)

@callback decode_stderr(binary(), parser_state()) :: decode_result()

decode_stdout(binary, parser_state)

@callback decode_stdout(binary(), parser_state()) :: decode_result()

handle_exit(term, parser_state)

@callback handle_exit(term(), parser_state()) :: decode_result()

id()

@callback id() :: id()

init_parser_state(keyword)

@callback init_parser_state(keyword()) :: parser_state()

transport_options(keyword)

@callback transport_options(keyword()) :: keyword()

Functions

accepts_input_after_start?(module)

@spec accepts_input_after_start?(module()) :: boolean()

Whether a lane can be given more input after its run has started.

This is the fact a caller needs to decide how to say something to a session already in flight: write to it, or interrupt the turn and resume the thread.

Two conditions, and both are required. The transport must leave stdin open, and the profile must declare :incremental_input — that it invokes the CLI in a mode which keeps reading stdin once the turn is running.

An open file descriptor is not the same as a reader. claude --print takes its prompt on argv, consumes stdin once while assembling that prompt, and never reads it again; the descriptor stays open for the life of the process and writing to it mid-turn reaches nobody. Deriving this from close_stdin_on_start? alone therefore reported claude as accepting live input, and a caller acting on that had its message silently swallowed while every layer above reported success. Verified against the shipping CLI: text piped before the turn changes the answer, and the same text written eight seconds in does not.

No shipped profile declares :incremental_input today, so every lane is steered by interrupt and resume. A profile that switches to a streaming input mode declares it and changes this with it.

It is not :interrupt or :resume from capabilities/0 — every profile declares those, and they do not distinguish the two mechanisms.

ensure_module(module)

@spec ensure_module(module()) ::
  :ok
  | {:error, {:module_not_loaded, module()}}
  | {:error, {:missing_callbacks, module(), [{atom(), non_neg_integer()}]}}
  | {:error, {:behaviour_not_declared, module()}}

Validates that a module satisfies the provider profile contract.

normalize_build_result(error)

@spec normalize_build_result(build_result()) ::
  {:ok, invocation(), teardown()} | {:error, term()}

Normalizes a build_invocation/1 return into {invocation, teardown}.

A profile that materializes nothing returns the two-element form, and gets a no-op teardown, so every caller can use one shape.

required_callbacks()

@spec required_callbacks() :: [callback_spec(), ...]

Returns the callbacks required by the provider profile contract.

validate_invocation(invocation)

@spec validate_invocation(invocation()) ::
  :ok
  | {:error, {:invalid_command, term()}}
  | {:error, {:invalid_args, term()}}
  | {:error, {:invalid_cwd, term()}}
  | {:error, {:invalid_env, term()}}
  | {:error, {:invalid_clear_env, term()}}

Validates a normalized invocation returned by a provider profile.