feat(agents): label subagent tool feedback
This commit is contained in:
parent
aea466b2b9
commit
7744e44704
6 changed files with 134 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue