mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
chatd resends unresolved tool calls on every retry and replica
takeover, and each resend of execute started a new process: one
command in CODAGT-757 started 36 times. An interrupt could neither
stop the process nor report what happened.
The agent now runs a request that carries Coder-Tool-Call-Id once
per chat and tool call ID and responds to repeats with the saved
response. StartProcess, EditFiles, and WriteFile send the header.
POST /api/v0/tool-calls/{id}/cancel refuses later requests for the
call, waits for a running one, stops its process, and returns the
saved response.
A process started with a tool call ID uses it as its process ID, and
its timeout_ms counts from the first start, so a retry does not
restart the timeout. Processes are keyed by chat ID: a chat sees only
its own processes, and a request without a chat ID sees only
processes started without one. Exited processes are kept for an hour
instead of 5 minutes so that retries can still read their output.
Requests without the header behave as before. Old agents ignore the
header and respond to a cancel with 404.
Refs CODAGT-757
120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/coder/coder/v2/agent/agentchat"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
|
|
"github.com/coder/coder/v2/coderd/tracing"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
"github.com/coder/coder/v2/httpmw"
|
|
)
|
|
|
|
func (a *agent) apiHandler() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Use(
|
|
httpmw.Recover(a.logger),
|
|
tracing.StatusWriterMiddleware,
|
|
// Reuse the coderd tracing middleware with a noop tracer (nil provider):
|
|
// it emits no spans or telemetry on the agent and only enriches the log
|
|
// context with client_session_id. Tracks the API, debug (for
|
|
// support-bundle correlation), and root routes.
|
|
tracing.Middleware(nil, []string{"/", "/api", "/api/**", "/debug/**"}, "agent"),
|
|
loggermw.Logger(a.logger, nil),
|
|
agentchat.Middleware,
|
|
a.toolCalls.Middleware,
|
|
)
|
|
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{
|
|
Message: "Hello from the agent!",
|
|
})
|
|
})
|
|
|
|
r.Mount("/api/v0", a.filesAPI.Routes())
|
|
r.Mount("/api/v0/git", a.gitAPI.Routes())
|
|
r.Mount("/api/v0/processes", a.processAPI.Routes())
|
|
r.Mount("/api/v0/tool-calls", a.toolCalls.Routes())
|
|
r.Mount("/api/v0/desktop", a.desktopAPI.Routes())
|
|
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
|
|
r.Mount("/api/v0/context-config", a.contextConfigAPI.Routes())
|
|
if a.contextAPI != nil {
|
|
r.Mount("/api/v0/context", a.contextAPI.Routes())
|
|
}
|
|
|
|
if a.devcontainers {
|
|
r.Mount("/api/v0/containers", a.containerAPI.Routes())
|
|
} else if manifest := a.manifest.Load(); manifest != nil && manifest.ParentID != uuid.Nil {
|
|
r.HandleFunc("/api/v0/containers", func(w http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
|
|
Message: "Dev Container feature not supported.",
|
|
Detail: "Dev Container integration inside other Dev Containers is explicitly not supported.",
|
|
})
|
|
})
|
|
} else {
|
|
r.HandleFunc("/api/v0/containers", func(w http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
|
|
Message: "Dev Container feature not enabled.",
|
|
Detail: "To enable this feature, set CODER_AGENT_DEVCONTAINERS_ENABLE=true in your template.",
|
|
})
|
|
})
|
|
}
|
|
|
|
promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger)
|
|
|
|
r.Get("/api/v0/listening-ports", a.listeningPortsHandler.handler)
|
|
r.Get("/api/v0/netcheck", a.HandleNetcheck)
|
|
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
|
|
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
|
|
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
|
|
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
|
|
r.Get("/debug/prometheus", promHandler.ServeHTTP)
|
|
|
|
return r
|
|
}
|
|
|
|
type ListeningPortsGetter interface {
|
|
GetListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error)
|
|
}
|
|
|
|
type listeningPortsHandler struct {
|
|
// In production code, this is set to an osListeningPortsGetter, but it can be overridden for
|
|
// testing.
|
|
getter ListeningPortsGetter
|
|
ignorePorts map[int]string
|
|
}
|
|
|
|
// handler returns a list of listening ports. This is tested by coderd's
|
|
// TestWorkspaceAgentListeningPorts test.
|
|
func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request) {
|
|
ports, err := lp.getter.GetListeningPorts()
|
|
if err != nil {
|
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Could not scan for listening ports.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
filteredPorts := make([]codersdk.WorkspaceAgentListeningPort, 0, len(ports))
|
|
for _, port := range ports {
|
|
if port.Port < workspacesdk.AgentMinimumListeningPort {
|
|
continue
|
|
}
|
|
|
|
// Ignore ports that we've been told to ignore.
|
|
if _, ok := lp.ignorePorts[int(port.Port)]; ok {
|
|
continue
|
|
}
|
|
filteredPorts = append(filteredPorts, port)
|
|
}
|
|
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceAgentListeningPortsResponse{
|
|
Ports: filteredPorts,
|
|
})
|
|
}
|