Refactor agent provider to allow support for custom agents#783
Refactor agent provider to allow support for custom agents#783
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
0e1956f to
5857c2d
Compare
| # - `use_default_provider!` | ||
| # | ||
| #: () -> Symbol | ||
| def valid_provider! |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Debated just hardcoding to claude. Still not sure.
There was a problem hiding this comment.
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
5857c2d to
618bd81
Compare
juniper-shopify
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
I don't like redefining the object in @values[:provider] from a provider name to a provider instance. Feels sneaky
| #: (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 |
There was a problem hiding this comment.
| #: (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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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) |
| @workflow_context = workflow_context #: WorkflowContext | ||
| @workflow_definition = File.read(workflow_path) #: String | ||
| @cog_registry = Cog::Registry.new #: Cog::Registry | ||
| @provider_registry = ProviderRegistry.new #: ProviderRegistry |
There was a problem hiding this comment.
| @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) |
There was a problem hiding this comment.
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?

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
usestatements (not implemented yet, coming next).