feat(agents): label subagent tool feedback

This commit is contained in:
Anton Bogdanovich 2026-05-06 17:55:31 -07:00
parent aea466b2b9
commit 7744e44704
6 changed files with 134 additions and 8 deletions

View file

@ -181,6 +181,44 @@ func shouldPublishToolFeedback(cfg *config.Config, ts *turnState) bool {
return cfg != nil && cfg.Agents.Defaults.IsToolFeedbackEnabled()
}
func toolFeedbackTitleForTurn(ts *turnState) string {
if ts == nil || !strings.HasPrefix(strings.TrimSpace(ts.sessionKey), "subturn-") {
return ""
}
if ts.agent == nil {
return "Subagent"
}
if title := displayAgentName(ts.agent.ID); title != "" {
return title
}
return "Subagent"
}
func displayAgentName(agentID string) string {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return ""
}
parts := strings.FieldsFunc(agentID, func(r rune) bool {
return r == '-' || r == '_' || r == ' '
})
var b strings.Builder
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteString(strings.ToUpper(part[:1]))
if len(part) > 1 {
b.WriteString(part[1:])
}
}
return b.String()
}
func cloneEventArguments(args map[string]any) map[string]any {
if len(args) == 0 {
return nil

View file

@ -94,6 +94,15 @@ toolLoop:
toolFeedbackExplanation,
toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen),
)
if title := toolFeedbackTitleForTurn(ts); title != "" {
feedbackMsg = utils.FormatToolFeedbackMessageWithStyleAndTitle(
al.cfg.Agents.Defaults.GetToolFeedbackStyle(),
title,
toolName,
toolFeedbackExplanation,
toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen),
)
}
fbCtx, fbCancel := context.WithTimeout(turnCtx, 3*time.Second)
_ = al.bus.PublishOutbound(fbCtx, outboundMessageForTurnWithKind(ts, feedbackMsg, messageKindToolFeedback))
fbCancel()
@ -376,6 +385,15 @@ toolLoop:
toolFeedbackExplanation,
toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen),
)
if title := toolFeedbackTitleForTurn(ts); title != "" {
feedbackMsg = utils.FormatToolFeedbackMessageWithStyleAndTitle(
al.cfg.Agents.Defaults.GetToolFeedbackStyle(),
title,
toolName,
toolFeedbackExplanation,
toolFeedbackArgsPreview(toolArgs, toolFeedbackMaxLen),
)
}
fbCtx, fbCancel := context.WithTimeout(turnCtx, 3*time.Second)
_ = al.bus.PublishOutbound(fbCtx, outboundMessageForTurnWithKind(ts, feedbackMsg, messageKindToolFeedback))
fbCancel()

View file

@ -247,7 +247,7 @@ func mergeToolFeedbackContent(previous, next string) string {
seen := make(map[string]struct{})
addLine := func(line string) {
line = strings.TrimSpace(line)
if line == "" || strings.EqualFold(line, "Working...") {
if line == "" || isWorkingSummaryHeader(line) {
return
}
if _, ok := seen[line]; ok {
@ -266,15 +266,35 @@ func mergeToolFeedbackContent(previous, next string) string {
if len(lines) > maxMergedToolFeedbackLines {
lines = lines[len(lines)-maxMergedToolFeedbackLines:]
}
if len(lines) == 0 {
return "Working..."
header := workingSummaryHeader(previous)
if candidate := workingSummaryHeader(next); candidate != "" {
header = candidate
}
return "Working...\n" + strings.Join(lines, "\n")
if header == "" {
header = "Working..."
}
if len(lines) == 0 {
return header
}
return header + "\n" + strings.Join(lines, "\n")
}
func isWorkingSummaryToolFeedback(content string) bool {
return isWorkingSummaryHeader(workingSummaryHeader(content))
}
func workingSummaryHeader(content string) string {
firstLine, _, _ := strings.Cut(strings.TrimSpace(content), "\n")
return strings.EqualFold(strings.TrimSpace(firstLine), "Working...")
firstLine = strings.TrimSpace(firstLine)
if isWorkingSummaryHeader(firstLine) {
return firstLine
}
return ""
}
func isWorkingSummaryHeader(line string) bool {
line = strings.TrimSpace(line)
return strings.EqualFold(line, "Working...") || strings.HasSuffix(strings.ToLower(line), " working...")
}
func toolFeedbackAnimationIntervalFor(content string) time.Duration {

View file

@ -165,3 +165,20 @@ func TestToolFeedbackAnimator_UpdateFailureRestoresTracking(t *testing.T) {
t.Fatalf("Current() after failed Update = (%q, %v), want (msg-1, true)", currentID, ok)
}
}
func TestMergeToolFeedbackContent_PreservesNamedWorkingSummaryHeader(t *testing.T) {
got := mergeToolFeedbackContent(
"Deep Research working...\n• tool: `read_file`",
"Deep Research working...\n• tool: `web_fetch`",
)
want := "Deep Research working...\n• tool: `read_file`\n• tool: `web_fetch`"
if got != want {
t.Fatalf("mergeToolFeedbackContent() = %q, want %q", got, want)
}
}
func TestIsWorkingSummaryToolFeedback_AcceptsNamedHeader(t *testing.T) {
if !isWorkingSummaryToolFeedback("Deep Research working...\n• tool: `read_file`") {
t.Fatal("expected named working summary to be recognized")
}
}

View file

@ -66,22 +66,41 @@ func FormatToolFeedbackMessage(toolName, explanation, argsPreview string) string
// progress UI should not leak internal paths, large JSON blobs, secrets, or
// model-drafted reasoning-like text.
func FormatToolFeedbackMessageWithStyle(style, toolName, explanation, argsPreview string) string {
return FormatToolFeedbackMessageWithStyleAndTitle(style, "", toolName, explanation, argsPreview)
}
func FormatToolFeedbackMessageWithStyleAndTitle(
style, title, toolName, explanation, argsPreview string,
) string {
if strings.EqualFold(strings.TrimSpace(style), ToolFeedbackStyleWorkingSummary) {
return FormatWorkingSummaryToolFeedbackMessage(toolName, argsPreview)
return FormatWorkingSummaryToolFeedbackMessageWithTitle(title, toolName, argsPreview)
}
return FormatToolFeedbackMessage(toolName, explanation, argsPreview)
}
func FormatWorkingSummaryToolFeedbackMessage(toolName, argsPreview string) string {
return FormatWorkingSummaryToolFeedbackMessageWithTitle("", toolName, argsPreview)
}
func FormatWorkingSummaryToolFeedbackMessageWithTitle(title, toolName, argsPreview string) string {
toolName = strings.TrimSpace(toolName)
header := workingSummaryHeader(title)
if toolName == "" {
return "Working..."
return header
}
line := fmt.Sprintf("• tool: `%s`", sanitizeToolFeedbackCodeSpan(toolName))
if summary := summarizeToolFeedbackArgs(toolName, argsPreview); summary != "" {
line += fmt.Sprintf(" — `%s`", sanitizeToolFeedbackCodeSpan(summary))
}
return "Working...\n" + line
return header + "\n" + line
}
func workingSummaryHeader(title string) string {
title = strings.TrimSpace(title)
if title == "" {
return "Working..."
}
return title + " working..."
}
func sanitizeToolFeedbackCodeSpan(text string) string {

View file

@ -54,6 +54,20 @@ func TestFormatToolFeedbackMessageWithStyle_WorkingSummary(t *testing.T) {
}
}
func TestFormatToolFeedbackMessageWithStyleAndTitle_WorkingSummary(t *testing.T) {
got := FormatToolFeedbackMessageWithStyleAndTitle(
"working_summary",
"DeepResearch",
"read_file",
"",
"{\"path\":\"README.md\"}",
)
want := "DeepResearch working...\n• tool: `read_file` — `README.md`"
if got != want {
t.Fatalf("FormatToolFeedbackMessageWithStyleAndTitle() = %q, want %q", got, want)
}
}
func TestFormatToolFeedbackMessageWithStyle_WorkingSummaryShowsFileBasenameOnly(t *testing.T) {
got := FormatToolFeedbackMessageWithStyle(
"working_summary",