mirror of
https://github.com/coder/coder.git
synced 2026-06-04 05:28:20 +00:00
1203f625b7
When the chat `start_workspace` tool triggers an active-version upgrade that introduces new required parameters, the build fails with a parameter validation error. Previously this returned a message telling the user to update from the UI — a dead end for the model. This PR lets the model recover inside the chat by: 1. Accepting an optional `parameters` map on `start_workspace` (same schema as `create_workspace`), forwarded as `RichParameterValues`. 2. Returning structured JSON error responses that preserve validation details and the workspace's `template_id`, so the model can call `read_template` to discover what changed. 3. Replacing the UI-only guidance in `exp_chats.go` with model-actionable retry instructions. The expected model flow on an active-version parameter failure is now: ``` start_workspace → fails (structured error with template_id + validations) read_template → discovers new required parameters start_workspace → retries with parameters map → workspace starts ``` <img width="846" height="511" alt="image" src="https://github.com/user-attachments/assets/d18b6864-5970-4225-8da0-0f2ab134ccb4" />
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
package chattool
|
|
|
|
import (
|
|
"encoding/json"
|
|
"unicode/utf8"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// toolResponse builds a fantasy.ToolResponse from a JSON-serializable
|
|
// result map. The map constraint ensures all tool results serialize
|
|
// to JSON objects so the frontend can safely parse them.
|
|
func toolResponse(result map[string]any) fantasy.ToolResponse {
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return fantasy.NewTextResponse("{}")
|
|
}
|
|
return fantasy.NewTextResponse(string(data))
|
|
}
|
|
|
|
// buildToolResponse marshals a buildErrorResult into a tool response.
|
|
// Separate from toolResponse to keep the map[string]any constraint
|
|
// on the general helper while allowing typed error structs.
|
|
func buildToolResponse(r buildErrorResult) fantasy.ToolResponse {
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
return fantasy.NewTextResponse("{}")
|
|
}
|
|
return fantasy.NewTextResponse(string(data))
|
|
}
|
|
|
|
// responseErrorResult converts a codersdk.Response into a structured
|
|
// tool result. We return these via toolResponse rather than
|
|
// NewTextErrorResponse because the fantasy/chatprompt pipeline flattens
|
|
// IsError content into a single string and drops validation details.
|
|
func responseErrorResult(resp codersdk.Response) map[string]any {
|
|
message := resp.Message
|
|
if message == "" {
|
|
message = "request failed"
|
|
}
|
|
|
|
result := map[string]any{
|
|
"error": message,
|
|
}
|
|
if resp.Detail != "" {
|
|
result["detail"] = resp.Detail
|
|
}
|
|
if len(resp.Validations) > 0 {
|
|
result["validations"] = resp.Validations
|
|
}
|
|
return result
|
|
}
|
|
|
|
func truncateRunes(value string, maxLen int) string {
|
|
if maxLen <= 0 || value == "" {
|
|
return ""
|
|
}
|
|
if utf8.RuneCountInString(value) <= maxLen {
|
|
return value
|
|
}
|
|
|
|
runes := []rune(value)
|
|
if maxLen > len(runes) {
|
|
maxLen = len(runes)
|
|
}
|
|
return string(runes[:maxLen])
|
|
}
|
|
|
|
// buildErrorResult is a structured error response that preserves
|
|
// the build ID alongside the error message. This lets the frontend
|
|
// keep showing build logs when a build fails instead of losing
|
|
// them on the error transition.
|
|
type buildErrorResult struct {
|
|
Error string `json:"error"`
|
|
BuildID string `json:"build_id,omitempty"`
|
|
}
|
|
|
|
func newBuildError(msg string, buildID uuid.UUID) buildErrorResult {
|
|
r := buildErrorResult{Error: msg}
|
|
if buildID != uuid.Nil {
|
|
r.BuildID = buildID.String()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// setBuildID adds the build_id field to a tool response map when
|
|
// the build ID is known (non-zero).
|
|
func setBuildID(result map[string]any, buildID uuid.UUID) {
|
|
if buildID != uuid.Nil {
|
|
result["build_id"] = buildID.String()
|
|
}
|
|
}
|
|
|
|
// setNoBuild marks the response with no_build: true when no build
|
|
// was triggered. The frontend uses this flag to suppress the
|
|
// build-log section for already-running workspaces.
|
|
func setNoBuild(result map[string]any, buildID uuid.UUID) {
|
|
if buildID == uuid.Nil {
|
|
result["no_build"] = true
|
|
}
|
|
}
|
|
|
|
// isTemplateAllowed checks whether a template ID is permitted by the
|
|
// configured allowlist. A nil function or an empty allowlist means
|
|
// all templates are allowed.
|
|
func isTemplateAllowed(getAllowlist func() map[uuid.UUID]bool, id uuid.UUID) bool {
|
|
if getAllowlist == nil {
|
|
return true
|
|
}
|
|
allowlist := getAllowlist()
|
|
if len(allowlist) == 0 {
|
|
return true
|
|
}
|
|
return allowlist[id]
|
|
}
|