Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/roast/cogs/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class MissingPromptError < AgentCogError; end
#
#: (Input) -> Output
def execute(input)
puts "[USER PROMPT] #{input.valid_prompt!}" if config.show_prompt?
puts "[USER PROMPT] #{input.prompts.first}" if config.show_prompt?
output = provider.invoke(input)
# NOTE: If progress is displayed, the agent's response will always be the last progress message,
# so showing it again is duplicative.
Expand Down
42 changes: 20 additions & 22 deletions lib/roast/cogs/agent/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ class Agent < Cog
# The agent cog requires a prompt that will be sent to the agent for processing.
# Optionally, a session identifier can be provided to maintain context across multiple invocations.
class Input < Cog::Input
# The prompt to send to the agent for processing
# The prompts to send to the agent for processing
#
#: String?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would it be helpful to mention that prompts share a single session? feels like that's the key insight for someone reading this for the first time

attr_accessor :prompt
# When multiple prompts are specified, each subsequent prompt is passed to the agent as soon as it completes
# the previous one, in the same session throughout. This can be useful for helping to ensure the agent produces
# final outputs in the form you desire after performing a long and complex task.
#
#: Array[String]
attr_accessor :prompts

# Optional session identifier for maintaining conversation context
#
Expand All @@ -28,7 +32,7 @@ class Input < Cog::Input
#: () -> void
def initialize
super
@prompt = nil #: String?
@prompts = [] #: Array[String]
end

# Validate that the input has all required parameters
Expand All @@ -37,41 +41,35 @@ def initialize
#
# #### See Also
# - `coerce`
# - `valid_prompt!`
#
#: () -> void
def validate!
valid_prompt!
raise Cog::Input::InvalidInputError, "At least one prompt is required" unless prompts.present?
raise Cog::Input::InvalidInputError, "Blank prompts are not allowed" if prompts.any?(&:blank?)
end

# Coerce the input from the return value of the input block
#
# If the input block returns a String, it will be used as the prompt value.
# If the input block returns an Array of Strings, the first will be used as the prompt and the
# rest will be used as finalizers.
#
# #### See Also
# - `validate!`
#
#: (untyped) -> void
def coerce(input_return_value)
if input_return_value.is_a?(String)
self.prompt ||= input_return_value
case input_return_value
when String
self.prompts = [input_return_value]
when Array
self.prompts = input_return_value.map(&:to_s)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

curious — this used to be ||= which protected explicitly-set prompts from being clobbered. was the switch to override intentional here, or a side effect of the refactor?

end
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we worry about nil/blank elements sneaking through here? the validate! check catches empty prompts arrays but individual blank entries could still pass through silently


# Get the validated prompt value
#
# Returns the prompt if it is present, otherwise raises an `InvalidInputError`.
#
# #### See Also
# - `prompt`
# - `validate!`
#
#: () -> String
def valid_prompt!
valid_prompt = @prompt
raise Cog::Input::InvalidInputError, "'prompt' is required" unless valid_prompt.present?

valid_prompt
#: (String) -> void
def prompt=(prompt)
@prompts = [prompt]
end
end
end
Expand Down
11 changes: 8 additions & 3 deletions lib/roast/cogs/agent/providers/claude.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ def initialize(invocation_result)

#: (Agent::Input) -> Agent::Output
def invoke(input)
invocation = ClaudeInvocation.new(@config, input)
invocation.run!
Output.new(invocation.result)
invocations = [] #: Array[ClaudeInvocation]
input.prompts.each do |prompt|
invocation = ClaudeInvocation.new(@config, prompt, invocations.last&.result&.session || input.session)
invocation.run!
invocations << invocation
break unless invocation.result.success
end
Output.new(invocations.last.not_nil!.result)
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/roast/cogs/agent/providers/claude/claude_invocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,20 @@ def initialize
end
end

#: (Agent::Config, Agent::Input) -> void
def initialize(config, input)
#: (Agent::Config, String, String?) -> void
def initialize(config, prompt, session)
@base_command = config.valid_command #: (String | Array[String])?
@model = config.valid_model #: String?
@append_system_prompt = config.valid_append_system_prompt #: String?
@replace_system_prompt = config.valid_replace_system_prompt #: String?
@apply_permissions = config.apply_permissions? #: bool
@working_directory = config.valid_working_directory #: Pathname?
@prompt = input.valid_prompt! #: String
@session = input.session #: String?
@context = Context.new #: Context
@result = Result.new #: Result
@raw_dump_file = config.valid_dump_raw_agent_messages_to_path #: Pathname?
@show_progress = config.show_progress? #: bool
@prompt = prompt
@session = session
end

#: () -> void
Expand Down
17 changes: 14 additions & 3 deletions lib/roast/cogs/agent/providers/pi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,20 @@ def initialize(invocation_result)

#: (Agent::Input) -> Agent::Output
def invoke(input)
invocation = PiInvocation.new(@config, input)
invocation.run!
Output.new(invocation.result)
invocations = [] #: Array[PiInvocation]
input.prompts.each do |prompt|
previous_session = invocations.last&.result&.session
invocation = PiInvocation.new(
@config,
prompt,
previous_session || input.session,
)
invocation.run!
invocations << invocation
break unless invocation.result.success
end
final_result = invocations.last.not_nil!.result
Output.new(final_result)
end
end
end
Expand Down
12 changes: 8 additions & 4 deletions lib/roast/cogs/agent/providers/pi/pi_invocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,21 @@ def initialize
end
end

#: (Agent::Config, Agent::Input) -> void
def initialize(config, input)
#: (Agent::Config, String, String?) -> void
def initialize(config, prompt, session)
@base_command = config.valid_command #: (String | Array[String])?
@model = config.valid_model #: String?
@append_system_prompt = config.valid_append_system_prompt #: String?
@replace_system_prompt = config.valid_replace_system_prompt #: String?
@working_directory = config.valid_working_directory #: Pathname?
@prompt = input.valid_prompt! #: String
@session = input.session #: String?
@prompt = prompt #: String
@session = session #: String?
@context = Context.new #: Context
@result = Result.new #: Result
@raw_dump_file = config.valid_dump_raw_agent_messages_to_path #: Pathname?
@show_prompt = config.show_prompt? #: bool
@show_progress = config.show_progress? #: bool
@show_response = config.show_response? #: bool
@num_turns = 0 #: Integer
@total_cost = 0.0 #: Float
@model_usage_accumulator = {} #: Hash[String, Hash[Symbol, Numeric]]
Expand All @@ -78,6 +80,7 @@ def run!
raise PiAlreadyStartedError if started?

@started = true
puts "[USER PROMPT] #{@prompt}" if @show_prompt
@start_time_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
_stdout, stderr, status = CommandRunner.execute(
command_line,
Expand All @@ -91,6 +94,7 @@ def run!
@completed = true
@result.success = true
finalize_stats!
puts "[AGENT RESPONSE] #{@result.response}" if @show_response
else
@failed = true
@result.success = false
Expand Down
4 changes: 4 additions & 0 deletions test/examples/functional/roast_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,14 @@ class RoastExamplesTest < FunctionalTest
# When show_progress is enabled (the default), text blocks are accumulated and printed
# as a single unit, and [AGENT RESPONSE] is suppressed to avoid duplication
expected_stdout = <<~STDOUT
[USER PROMPT] What is the world's largest lake?
[USER PROMPT] What is the world's largest lake?
Caspian spreads wide—
Ancient waters vast and deep,
World's largest lake gleams.
[AGENT RESPONSE] Caspian spreads wide—
Ancient waters vast and deep,
World's largest lake gleams.
[AGENT STATS] Turns: 1
Duration: 0 seconds
Cost (USD): $0.024634
Expand Down
83 changes: 52 additions & 31 deletions test/roast/cogs/agent/input_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ def setup
@input = Input.new
end

test "initialize sets prompt to nil" do
assert_nil @input.prompt
test "initialize sets prompts to empty array" do
assert_equal [], @input.prompts
end

test "prompt can be set" do
test "prompt= sets prompts to single-element array" do
@input.prompt = "What is 2+2?"

assert_equal "What is 2+2?", @input.prompt
assert_equal ["What is 2+2?"], @input.prompts
end

test "prompts can be set directly" do
@input.prompts = ["First", "Second"]

assert_equal ["First", "Second"], @input.prompts
end

test "session can be set" do
Expand All @@ -26,79 +32,94 @@ def setup
assert_equal "session-123", @input.session
end

test "validate! raises error when prompt is nil" do
test "validate! raises error when prompts is empty" do
error = assert_raises(Cog::Input::InvalidInputError) do
@input.validate!
end

assert_equal "'prompt' is required", error.message
assert_equal "At least one prompt is required", error.message
end

test "validate! raises error when prompt is empty string" do
@input.prompt = ""
test "validate! raises error when any prompt is blank" do
@input.prompts = ["Valid prompt", " ", "Another"]

error = assert_raises(Cog::Input::InvalidInputError) do
@input.validate!
end

assert_equal "'prompt' is required", error.message
assert_equal "Blank prompts are not allowed", error.message
end

test "validate! raises error when prompt is whitespace only" do
@input.prompt = " "
test "validate! succeeds when prompts has at least one element" do
@input.prompt = "What is 2+2?"

error = assert_raises(Cog::Input::InvalidInputError) do
assert_nothing_raised do
@input.validate!
end

assert_equal "'prompt' is required", error.message
end

test "validate! succeeds when prompt is present" do
@input.prompt = "What is 2+2?"
test "validate! succeeds with multiple prompts" do
@input.prompts = ["First", "Second"]

assert_nothing_raised do
@input.validate!
end
end

test "coerce sets prompt from string" do
test "coerce sets prompts from string" do
@input.coerce("What is the meaning of life?")

assert_equal "What is the meaning of life?", @input.prompt
assert_equal ["What is the meaning of life?"], @input.prompts
end

test "coerce does not override existing prompt" do
test "coerce overrides existing prompts" do
@input.prompt = "Original prompt"
@input.coerce("New prompt")

assert_equal "Original prompt", @input.prompt
assert_equal ["New prompt"], @input.prompts
end

test "coerce does nothing for non-string values" do
test "coerce does nothing for non-string non-array values" do
@input.coerce(42)

assert_nil @input.prompt
assert_equal [], @input.prompts
end

test "coerce does nothing for nil" do
@input.coerce(nil)

assert_nil @input.prompt
assert_equal [], @input.prompts
end

test "valid_prompt! returns prompt when present" do
@input.prompt = "Test prompt"
test "coerce with array sets all prompts" do
@input.coerce(["Main prompt", "Finalizer 1", "Finalizer 2"])

assert_equal "Test prompt", @input.valid_prompt!
assert_equal ["Main prompt", "Finalizer 1", "Finalizer 2"], @input.prompts
end

test "valid_prompt! raises error when prompt is nil" do
error = assert_raises(Cog::Input::InvalidInputError) do
@input.valid_prompt!
end
test "coerce with single-element array" do
@input.coerce(["Only prompt"])

assert_equal ["Only prompt"], @input.prompts
end

test "coerce with array converts elements to strings" do
@input.coerce(["Main prompt", 42, :symbol])

assert_equal ["Main prompt", "42", "symbol"], @input.prompts
end

test "coerce with empty array sets prompts to empty" do
@input.coerce([])

assert_equal [], @input.prompts
end

test "prompt= overrides all existing prompts" do
@input.prompts = ["First", "Second", "Third"]
@input.prompt = "Only"

assert_equal "'prompt' is required", error.message
assert_equal ["Only"], @input.prompts
end
end
end
Expand Down
Loading
Loading