Skip to content

Refactor agent provider to allow support for custom agents#783

Open
dersam wants to merge 1 commit intomainfrom
02-12-move_agent_providers_into_shared_registry
Open

Refactor agent provider to allow support for custom agents#783
dersam wants to merge 1 commit intomainfrom
02-12-move_agent_providers_into_shared_registry

Conversation

@dersam
Copy link
Contributor

@dersam dersam commented Feb 12, 2026

This will allow us to support agents other than Claude Code, though that's not implemented yet.

Primarily, what this does is move provider definitions to a registry on the workflow, rather than a hardcoded array. Roast will continue to define a select set of default providers such as Claude Code (and opencode, soon). Third parties can add their own definitions in gems, and these providers will be pulled in on the necessary workflows via use statements (not implemented yet, coming next).

Copy link
Contributor Author

dersam commented Feb 12, 2026

@dersam dersam force-pushed the 02-12-move_agent_providers_into_shared_registry branch from 0e1956f to 5857c2d Compare February 12, 2026 20:16
# - `use_default_provider!`
#
#: () -> Symbol
def valid_provider!
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation is done by the registry now.

# see if we need a way to define hooks for custom cog data to be distributed
# at config time.
# NOTE: This must happen after all merges, since merge creates new Config instances.
if config.is_a?(Cogs::Agent::Config)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really didn't want to pass the provider registry around, and I also wanted to make it a little difficult to get at. So it's only available on this particular config, and even then only injected invisibly by the manager.

I might be overthinking this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't love that we're special-casing the agent cog here. But, I agree that it's not worth over-engineering a provider/extension/plugin framework for every cog type up front.

What if we created a CogResourceProviderContext or something like that, that we could pass to the ConfigManager and the ExecutionManager, like we do with the WorkflowContext object (or just use that object), and then the execution manager could pull the agent provider instance from the agent provider registry in that context object. keep the config object pure and only containing a symbol.

That also solves your problem of making it a little more difficult to get at -- it'd be on a totally different object than the config. something that isn't exposed in user-land

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, and I think it'll let me solve some of the other problems around config more easily. Will investigate.


def initialize
@providers = {} #: Hash[Symbol, singleton(Cogs::Agent::Provider)]
@default = ENV["ROAST_DEFAULT_AGENT"]&.to_sym || :claude
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debated just hardcoding to claude. Still not sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't think there's value in the env var here.

In your workflow, you can of course do this if you want to set the default agent for your workflow explicitly in the workflow

config do
  agent { provider :whatever_you_want }
end

or this if you actually want to make it dependent on an env var at runtime

config do
  agent { provider ENV["ROAST_AGENT"]&.to_sym || :claude }
end

This will allow us to support agents other than Claude Code
Copy link
Contributor

@juniper-shopify juniper-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the overall approach, but I don't love the way it integrates with the config object. See my suggestions in line about including the provider registry on the workflow context object (or adding a new "cog resource context" object in parallel to it)

def execute(input)
puts "[USER PROMPT] #{input.valid_prompt!}" if config.show_prompt?
output = provider.invoke(input)
output = config.values[:provider].invoke(input)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how I feel about storing the actual provider instance in the config object.

Also, I'd still prefer to access it from the config object via something like the valid_provider! method, instead of directly from the values hash


def validate!
# Provider registry is responsible for ensuring the validity of providers
@values[:provider] = @provider_registry.fetch(@values[:provider]).new(self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like redefining the object in @values[:provider] from a provider name to a provider instance. Feels sneaky

Comment on lines -11 to +14
#: (Cog::Registry, Array[^() -> void]) -> void
def initialize(cog_registry, config_procs)
#: (Cog::Registry, Array[^() -> void], ProviderRegistry) -> void
def initialize(cog_registry, config_procs, provider_registry)
@cog_registry = cog_registry
@provider_registry = provider_registry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#: (Cog::Registry, Array[^() -> void]) -> void
def initialize(cog_registry, config_procs)
#: (Cog::Registry, Array[^() -> void], ProviderRegistry) -> void
def initialize(cog_registry, config_procs, provider_registry)
@cog_registry = cog_registry
@provider_registry = provider_registry
#: (Cog::Registry, Array[^() -> void], Cogs::Agent::ProviderRegistry) -> void
def initialize(cog_registry, config_procs, agent_provider_registry)
@cog_registry = cog_registry
@agent_provider_registry = agent_provider_registry

I think we should make it clear that the scope of the provider is the agent cog.

(but, see comment below, I think we should pass the registry around some other way than via the config object directly)

# see if we need a way to define hooks for custom cog data to be distributed
# at config time.
# NOTE: This must happen after all merges, since merge creates new Config instances.
if config.is_a?(Cogs::Agent::Config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't love that we're special-casing the agent cog here. But, I agree that it's not worth over-engineering a provider/extension/plugin framework for every cog type up front.

What if we created a CogResourceProviderContext or something like that, that we could pass to the ConfigManager and the ExecutionManager, like we do with the WorkflowContext object (or just use that object), and then the execution manager could pull the agent provider instance from the agent provider registry in that context object. keep the config object pure and only containing a symbol.

That also solves your problem of making it a little more difficult to get at -- it'd be on a totally different object than the config. something that isn't exposed in user-land

# Maintains the list of registered agent providers
# Built in providers are registered automatically.
# Custom agents are registered per workflow with `use`.)
class ProviderRegistry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class ProviderRegistry
module Cogs
class Agent < Cog
class ProviderRegistry

let's namespace this into the Agent, since it's Agent-specific


def initialize
@providers = {} #: Hash[Symbol, singleton(Cogs::Agent::Provider)]
@default = ENV["ROAST_DEFAULT_AGENT"]&.to_sym || :claude
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't think there's value in the env var here.

In your workflow, you can of course do this if you want to set the default agent for your workflow explicitly in the workflow

config do
  agent { provider :whatever_you_want }
end

or this if you actually want to make it dependent on an env var at runtime

config do
  agent { provider ENV["ROAST_AGENT"]&.to_sym || :claude }
end

@providers[name] = provider_class
end

def fetch(name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sig

@workflow_context = workflow_context #: WorkflowContext
@workflow_definition = File.read(workflow_path) #: String
@cog_registry = Cog::Registry.new #: Cog::Registry
@provider_registry = ProviderRegistry.new #: ProviderRegistry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@provider_registry = ProviderRegistry.new #: ProviderRegistry

if you just make the workflow context expose an agent_provider_registry, initialized to ProviderRegistry.new then we don't need this ivar at all


# Register the built in agent providers.
def add_providers!
@provider_registry.register(Roast::Cogs::Agent::Providers::Claude, :claude)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put the method that registers the built-in providers in the ProviderRegistry class itself, and either call it automatically from its constructor or just call provider_registry.register_built_in_providers! here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants