Skip to content
Draft
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
20 changes: 20 additions & 0 deletions lib/roast/cogs/agent/providers/claude/claude_invocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ClaudeFailedError < ClaudeInvocationError; end
class Context
def initialize
@tool_uses = {} #: Hash[String, Messages::ToolUseMessage]
@task_subjects = {} #: Hash[String, String]
end

#: (String?) -> Messages::ToolUseMessage?
Expand All @@ -32,6 +33,23 @@ def add_tool_use(tool_use_message)
id = tool_use_message.id
@tool_uses[id] = tool_use_message if id
end

#: (Messages::ToolResultMessage) -> void
def add_tool_result(tool_result_message)
return if tool_result_message.is_error

tool_use_message = tool_use(tool_result_message.tool_use_id)
return unless tool_use_message && tool_use_message.name == :taskcreate

task_id = tool_result_message.content.to_s[/\ATask #(\d+) created successfully\b/, 1]
subject = tool_use_message.input[:subject]
@task_subjects[task_id] = subject if task_id && subject.present?
end

#: ((String | Integer)?) -> String?
def task_subject(task_id)
@task_subjects[task_id.to_s] if task_id
end
end

class Result
Expand Down Expand Up @@ -149,6 +167,8 @@ def handle_message(message)
@result.stats = message.stats
when Messages::ToolUseMessage
@context.add_tool_use(message)
when Messages::ToolResultMessage
@context.add_tool_result(message)
when Messages::UserMessage
message.messages.each { |msg| handle_message(msg) }
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def initialize(type:, hash:)

#: (ClaudeInvocation::Context) -> String?
def format(context)
tool_use = ToolUse.new(name:, input:)
task_subject = context.task_subject(input[:taskId]) if name == :taskupdate
tool_use = ToolUse.new(name:, input:, task_subject:)
tool_use.format
end
end
Expand Down
17 changes: 10 additions & 7 deletions lib/roast/cogs/agent/providers/claude/tool_use.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class ToolUse
#: Hash[Symbol, untyped]
attr_reader :input

#: (name: Symbol, input: Hash[Symbol, untyped]) -> void
def initialize(name:, input:)
#: (name: Symbol, input: Hash[Symbol, untyped], ?task_subject: String?) -> void
def initialize(name:, input:, task_subject: nil)
@name = name
@input = input
@task_subject = task_subject
end

#: () -> String
Expand Down Expand Up @@ -320,17 +321,19 @@ def format_agent
# :taskId (Integer) – id of the task to update [required]
# :status (String) – the task's new status [required]
#
# Output: "TASKUPDATE #<taskId> → <status>" — the id is prefixed with
# "#" and joined to the status with " → ". Both fields are always
# shown and neither is truncated.
# Output: 'TASKUPDATE "<subject>" → <status>' when the task's subject
# was observed in this invocation. Falls back to
# "TASKUPDATE #<taskId> → <status>" when it was not. Subjects are
# truncated to TRUNCATE_LIMIT chars and quoted.
#
# Examples:
# TASKUPDATE #1 → completed
# TASKUPDATE "Run formatter stress test" → completed
# TASKUPDATE #2 → in_progress
#
#: () -> String
def format_taskupdate
"TASKUPDATE ##{input[:taskId]} → #{input[:status]}"
task = @task_subject.present? ? truncate(@task_subject).inspect : "##{input[:taskId]}"
"TASKUPDATE #{task} → #{input[:status]}"
end

# Formats a TaskCreate tool-use line.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,44 @@ def failure_status
assert_nil context.tool_use(nil)
end

test "Context#add_tool_result ignores failed task creation" do
context = Claude::ClaudeInvocation::Context.new
task_create = Claude::Messages::ToolUseMessage.new(
type: :tool_use,
hash: { id: "create_123", name: "TaskCreate", input: { subject: "Read full code" } },
)
task_created = Claude::Messages::ToolResultMessage.new(
type: :tool_result,
hash: {
tool_use_id: "create_123",
content: "Task #1 created successfully",
is_error: true,
},
)
context.add_tool_use(task_create)

context.add_tool_result(task_created)

assert_nil context.task_subject("1")
end

test "Context#add_tool_result ignores an unexpected task creation response" do
context = Claude::ClaudeInvocation::Context.new
task_create = Claude::Messages::ToolUseMessage.new(
type: :tool_use,
hash: { id: "create_123", name: "TaskCreate", input: { subject: "Read full code" } },
)
task_created = Claude::Messages::ToolResultMessage.new(
type: :tool_result,
hash: { tool_use_id: "create_123", content: "Task creation queued" },
)
context.add_tool_use(task_create)

context.add_tool_result(task_created)

assert_nil context.task_subject("1")
end

test "Result initializes with empty response and success false" do
result = Claude::ClaudeInvocation::Result.new

Expand Down Expand Up @@ -322,6 +360,61 @@ def failure_status
assert_equal tool_use_message, context.tool_use("tool_123")
end

test "handle_message uses the created task subject when formatting a task update" do
@config.show_progress!
invocation = Claude::ClaudeInvocation.new(@config, "Test prompt", nil)
task_create = Claude::Messages::ToolUseMessage.new(
type: :tool_use,
hash: {
id: "create_123",
name: "TaskCreate",
input: { subject: "Read full code" },
},
)
task_created = Claude::Messages::ToolResultMessage.new(
type: :tool_result,
hash: {
tool_use_id: "create_123",
content: "Task #1 created successfully",
},
)
task_update = Claude::Messages::ToolUseMessage.new(
type: :tool_use,
hash: {
id: "update_123",
name: "TaskUpdate",
input: { taskId: 1, status: "completed" },
},
)

stdout, = capture_io do
invocation.send(:handle_message, task_create)
invocation.send(:handle_message, task_created)
invocation.send(:handle_message, task_update)
end

assert_includes stdout.lines, "TASKUPDATE \"Read full code\" → completed\n"
end

test "handle_message falls back to the task id when the create result is unavailable" do
@config.show_progress!
invocation = Claude::ClaudeInvocation.new(@config, "Test prompt", nil)
task_update = Claude::Messages::ToolUseMessage.new(
type: :tool_use,
hash: {
id: "update_123",
name: "TaskUpdate",
input: { taskId: "1", status: "completed" },
},
)

stdout, = capture_io do
invocation.send(:handle_message, task_update)
end

assert_equal "TASKUPDATE #1 → completed\n", stdout
end

test "handle_message processes AssistantMessage recursively" do
text_hash = { type: :text, text: "Hello" }
assistant_message = Claude::Messages::AssistantMessage.new(
Expand Down
12 changes: 12 additions & 0 deletions test/roast/cogs/agent/providers/claude/tool_use_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,18 @@ class Claude::ToolUseTest < ActiveSupport::TestCase
assert_equal "TASKUPDATE #1 → completed", output
end

test "format_taskupdate renders and escapes a known task subject" do
tool_use = Claude::ToolUse.new(
name: :taskupdate,
input: { taskId: 1, status: "completed" },
task_subject: 'Read "full" code',
)

output = tool_use.format

assert_equal 'TASKUPDATE "Read \\"full\\" code" → completed', output
end

# format_taskcreate

test "format_taskcreate renders the subject" do
Expand Down
Loading