add the greeting msg
Some checks failed
CI / test (push) Failing after 4m41s
CI / fixture (push) Failing after 9s

This commit is contained in:
2026-10-05 16:20:17 +05:30
parent 939598a187
commit a372340281
8 changed files with 1043 additions and 32 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/domain"
"github.com/krow/krow-backend/go-api/internal/gateway"
"github.com/krow/krow-backend/go-api/internal/runtime"
"github.com/krow/krow-backend/go-api/internal/tools"
)
@@ -163,6 +164,7 @@ func (s *Server) handleAgentRun(w http.ResponseWriter, r *http.Request) {
return
}
s.logUnsaved(ident, res)
s.logGatewayFailure(ident, res)
writeJSON(w, http.StatusOK, buildRunResponse(res))
}
@@ -180,6 +182,43 @@ func (s *Server) logUnsaved(ident authctx.Identity, res *runtime.ExecutionResult
}
}
// logGatewayFailure is the operator's record of the model provider not
// answering, and it exists because the reader of the message cannot report what
// the message does not say.
//
// Four of the five gateway faults need an administrator and will fail
// identically on every retry — a rejected credential, a model id this
// deployment cannot use, no key at all, and a 4xx from the endpoint. The person
// in the chat panel is told, correctly, that retrying will not help; but
// nothing until now told the side that CAN fix it. A deployment failing every
// run produced a stream of 200s and no error line, so the only record of which
// fault it was lived in a trajectory somebody had to know to go and read.
//
// Logged at Error because that is what it is: on a rate limit it is a capacity
// decision worth seeing, and on the other four it is an outage. It carries the
// gateway's code and status and NOT the message — §10 keeps model and document
// text out of the log store, and the code is the part that is actionable
// anyway.
func (s *Server) logGatewayFailure(ident authctx.Identity, res *runtime.ExecutionResult) {
if res.Termination != runtime.TerminationGatewayFailure {
return
}
// Empty rather than invented when the cause did not survive: "which fault
// was it" is the whole point of this line, and a guessed answer to it is
// worse than a visible gap.
code, status := "", 0
var gwErr *gateway.Error
if errors.As(res.Error, &gwErr) {
code, status = gwErr.Code, gwErr.Status
}
s.log.Error("gateway failure",
"run_id", res.RunID, "tenant_id", ident.OrgID,
"agent_key", res.AgentID, "agent_version", res.AgentVersion,
"gateway_code", code, "gateway_status", status)
}
// buildRunResponse turns a runtime result into the client's shape.
//
// Every termination answers 200. That looks wrong at first and is not: the
@@ -206,7 +245,7 @@ func buildRunResponse(res *runtime.ExecutionResult) runResponse {
},
}
if res.Termination != runtime.TerminationCompleted {
out.Message = terminationMessage(res.Termination)
out.Message = terminationMessage(res.Termination, res.Error)
}
return out
}
@@ -222,7 +261,9 @@ func buildRunResponse(res *runtime.ExecutionResult) runResponse {
// Every one of the seven is spelled out. A default that said "something went
// wrong" would be the place where a Refused run and a ToolFailure became
// indistinguishable to the person best placed to tell us which it was.
func terminationMessage(t runtime.Termination) string {
// `cause` is the run's own error, carried so GatewayFailure can say which
// gateway failure it was. Every other termination ignores it.
func terminationMessage(t runtime.Termination, cause error) string {
switch t {
case runtime.TerminationCompleted:
return ""
@@ -239,16 +280,70 @@ func terminationMessage(t runtime.Termination) string {
case runtime.TerminationRefused:
return "The agent declined to answer this one."
case runtime.TerminationGatewayFailure:
// The one termination where "try again" is honest advice: the
// dominant cause is a rate limit that clears within a minute, and
// nothing about the question itself was the problem.
return "The model behind this agent did not answer — usually it is busy. " +
"Wait a minute and ask again. Nothing was changed."
return gatewayFailureMessage(cause)
default:
return "The agent did not finish."
}
}
// gatewayFailureMessage tells a GatewayFailure apart from the four others it
// used to be indistinguishable from.
//
// GatewayFailure is everything the gateway can raise except a refusal and a
// timeout, which have terminations of their own. That is five different faults,
// and the one sentence they all produced was "usually it is busy — wait a
// minute and ask again".
//
// For a rate limit that is true and useful. For the other three it is advice
// that CANNOT work: a credential the provider rejected, a model id that is not
// on the configured endpoint, and no key at all will each fail identically on
// every retry, forever. Telling somebody to wait a minute for a misconfigured
// deployment sends them round a loop with no exit, and hides an operator
// problem behind what reads as a transient one — the reader retries instead of
// reporting it, so nobody with access to fix it ever hears.
//
// So each says what it is, and only the two that clear on their own invite a
// retry. The wording stays free of vendor names and status codes: the person
// reading it cannot act on "429 from the model endpoint", and the code is in
// the trajectory for the person who can.
func gatewayFailureMessage(cause error) string {
var gwErr *gateway.Error
if !errors.As(cause, &gwErr) {
// No gateway error to read — either the cause was lost or something
// non-gateway reached this termination. The old sentence is still the
// best guess, so it is what an unknown falls back to.
return "The model behind this agent did not answer — usually it is busy. " +
"Wait a minute and ask again. Nothing was changed."
}
switch gwErr.Code {
case gateway.CodeRateLimited:
return "The model behind this agent is busy right now. " +
"Wait a minute and ask again. Nothing was changed."
case gateway.CodeUpstream:
if gwErr.Status >= 500 {
return "The model behind this agent is having trouble. " +
"Try again in a few minutes. Nothing was changed."
}
return "The model behind this agent could not be reached, and retrying is " +
"unlikely to help. This needs an administrator. Nothing was changed."
case gateway.CodeUnauthorized:
return "This deployment's model credentials were rejected, so the agent " +
"cannot answer. Retrying will not help — it needs an administrator. " +
"Nothing was changed."
case gateway.CodeInvalidRequest:
return "The agent is pointed at a model this deployment cannot use, so it " +
"cannot answer. Retrying will not help — it needs an administrator. " +
"Nothing was changed."
case gateway.CodeNotConfigured:
return "No model is configured for this deployment, so the agent cannot " +
"answer. It needs an administrator. Nothing was changed."
default:
return "The model behind this agent did not answer — usually it is busy. " +
"Wait a minute and ask again. Nothing was changed."
}
}
// runLoadError maps a pre-run failure onto the API's error vocabulary.
//
// These are the errors from LoadExecutableAgent, raised before any run began —
@@ -351,6 +446,7 @@ func (s *Server) streamAgentRun(w http.ResponseWriter, r *http.Request, ident au
return
}
s.logUnsaved(ident, res)
s.logGatewayFailure(ident, res)
writeJSON(w, http.StatusOK, buildRunResponse(res))
return
}
@@ -399,6 +495,7 @@ func (s *Server) streamAgentRun(w http.ResponseWriter, r *http.Request, ident au
}
s.logUnsaved(ident, res)
s.logGatewayFailure(ident, res)
send(map[string]any{"run": buildRunResponse(res)})
fmt.Fprint(w, "data: [DONE]\n\n")
flusher.Flush()