Files
Michael Suchacz 47b90afce6 fix(coderd/x/chatd/chatadvisor): truncate oversized advisor questions (#25489)
Advisor tool calls currently reject questions over 2000 runes, which can
leave the parent model retrying the same invalid call.

This documents the limit in the advisor tool schema and guidance, then
truncates oversized questions rune-safely before building the nested
advisor prompt.

> Mux working on behalf of Mike.
2026-05-19 17:57:14 +02:00

76 lines
2.8 KiB
Go

package chatadvisor
import (
"context"
"encoding/json"
"strings"
"charm.land/fantasy"
)
// ToolName is the identifier the advisor tool registers under. The parent
// agent's exclusive-tool policy and the advisor-guidance block both reference
// this name, so keeping them synchronized requires a single source of truth.
const ToolName = "advisor"
// advisorQuestionMaxRunes caps the parent agent's question at a length
// that leaves room in the advisor prompt for system preamble and recent
// conversation context.
const advisorQuestionMaxRunes = 2000
// ToolOptions configures the built-in advisor tool.
type ToolOptions struct {
Runtime *Runtime
GetConversationSnapshot func() []fantasy.Message
PublishAdviceDelta func(toolCallID string, delta string)
PublishAdviceReset func(toolCallID string)
}
// Tool returns a fantasy.AgentTool that asks a nested model for concise
// strategic guidance. The nested advisor sees recent conversation
// context, runs without tools, and is limited to a single model step.
func Tool(opts ToolOptions) fantasy.AgentTool {
return fantasy.NewAgentTool(
ToolName,
"Ask a separate advisor pass for strategic guidance about planning, architecture, tradeoffs, or debugging strategy. Provide a brief question of 2000 runes or fewer, summarizing context instead of pasting long logs or transcripts. The advisor sees recent conversation context, runs without tools for a single step, and responds to the parent agent rather than the end user.",
func(ctx context.Context, args AdvisorArgs, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if opts.Runtime == nil {
return fantasy.NewTextErrorResponse("advisor runtime is not configured"), nil
}
if opts.GetConversationSnapshot == nil {
return fantasy.NewTextErrorResponse("conversation snapshot provider is not configured"), nil
}
question := strings.TrimSpace(args.Question)
if question == "" {
return fantasy.NewTextErrorResponse("question is required"), nil
}
var runOpts *RunAdvisorOptions
if call.ID != "" && (opts.PublishAdviceDelta != nil || opts.PublishAdviceReset != nil) {
runOpts = &RunAdvisorOptions{}
if opts.PublishAdviceDelta != nil {
runOpts.OnAdviceDelta = func(delta string) {
opts.PublishAdviceDelta(call.ID, delta)
}
}
if opts.PublishAdviceReset != nil {
runOpts.OnAdviceReset = func() {
opts.PublishAdviceReset(call.ID)
}
}
}
result, err := opts.Runtime.RunAdvisor(ctx, question, opts.GetConversationSnapshot(), runOpts)
if err != nil {
return fantasy.NewTextErrorResponse(err.Error()), nil
}
data, err := json.Marshal(result)
if err != nil {
return fantasy.NewTextResponse("{}"), nil
}
return fantasy.NewTextResponse(string(data)), nil
},
)
}