mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
e56381eb61
Stream advisor output into the advisor tool card while the nested advisor call is still running. This keeps the advisor implementation intentionally advisor-specific: the parent model still receives the same final structured tool result, while the frontend receives transient `tool-result.result_delta` parts to render partial advisor text in the expanded card. The final persisted chat history remains unchanged. Refs CODAGT-322. Generated by Coder Agents. <details> <summary>Implementation plan</summary> - Publish advisor text deltas from the nested `chatloop.Run` via `RunAdvisorOptions.OnAdviceDelta`. - Forward those deltas through `chatadvisor.Tool` with the parent advisor tool call ID. - Emit transient `ChatMessagePartTypeToolResult` websocket parts with `ResultDelta` from `chatd`. - Add `result_delta` to the generated tool-result TypeScript variant. - Accumulate tool result deltas in frontend stream state and keep the tool running until the final result arrives. - Render streamed advisor advice in the existing advisor card using streaming markdown mode, while retaining the updated advisor UI. </details>
83 lines
2.9 KiB
Go
83 lines
2.9 KiB
Go
package chatadvisor
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"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. 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
|
|
}
|
|
if utf8.RuneCountInString(question) > advisorQuestionMaxRunes {
|
|
return fantasy.NewTextErrorResponse(
|
|
fmt.Sprintf("question must be %d runes or fewer", advisorQuestionMaxRunes),
|
|
), 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
|
|
},
|
|
)
|
|
}
|