v1.6.0
release-tag / release-image (push) Successful in 6m50s

This commit is contained in:
2026-09-02 10:26:50 +02:00
parent 8c4ce2d6c2
commit 9a4370e4df
272 changed files with 31244 additions and 5980 deletions
+104 -14
View File
@@ -92,6 +92,8 @@ func (s *Server) routes() {
s.mux.Handle("POST /api/v1/integrations/outcomes/search", s.integrationAuth(http.HandlerFunc(s.integrationValidatedOutcomeSearch)))
s.mux.Handle("GET /api/v1/integrations/graph/research", s.controlReadAuth(http.HandlerFunc(s.integrationResearchGraph)))
s.mux.Handle("GET /api/v1/integrations/graph/brain", s.controlReadAuth(http.HandlerFunc(s.integrationBrainGraph)))
s.mux.Handle("GET /api/v1/integrations/graph/status", s.controlReadAuth(http.HandlerFunc(s.integrationGraphStatus)))
s.mux.Handle("GET /api/v1/integrations/orchestrator/status", s.controlReadAuth(http.HandlerFunc(s.integrationOrchestratorStatus)))
s.mux.Handle("POST /internal/v1/cluster/request-vote", s.clusterAuth(http.HandlerFunc(s.clusterRequestVote)))
s.mux.Handle("POST /internal/v1/cluster/heartbeat", s.clusterAuth(http.HandlerFunc(s.clusterHeartbeat)))
@@ -102,6 +104,8 @@ func (s *Server) routes() {
s.mux.Handle("GET /internal/v1/cluster/decision/{id}", s.clusterAuth(http.HandlerFunc(s.clusterDecision)))
s.mux.Handle("GET /internal/v1/cluster/status", s.clusterAuth(http.HandlerFunc(s.clusterStatus)))
s.mux.Handle("POST /api/v1/worker/register", s.workerAuth(http.HandlerFunc(s.workerRegister)))
s.mux.Handle("POST /api/v1/worker/heartbeat", s.workerAuth(http.HandlerFunc(s.workerHeartbeat)))
s.mux.Handle("POST /api/v1/worker/claim", s.workerAuth(http.HandlerFunc(s.workerClaim)))
s.mux.Handle("POST /api/v1/worker/complete", s.workerAuth(http.HandlerFunc(s.workerComplete)))
@@ -138,6 +142,12 @@ func (s *Server) routes() {
s.mux.Handle("GET /admin/api/knowledge/memories", s.adminAuth(http.HandlerFunc(s.adminKnowledgeMemories)))
s.mux.Handle("GET /admin/api/knowledge/memory/{id}", s.adminAuth(http.HandlerFunc(s.adminKnowledgeMemory)))
s.mux.Handle("GET /admin/api/knowledge/graph", s.adminAuth(http.HandlerFunc(s.adminKnowledgeGraph)))
s.mux.Handle("GET /admin/api/graph/status", s.adminAuth(http.HandlerFunc(s.adminGraphStatus)))
s.mux.Handle("POST /admin/api/graph/backfill", s.adminAuth(http.HandlerFunc(s.adminGraphBackfill)))
s.mux.Handle("GET /admin/api/orchestrator/status", s.adminAuth(http.HandlerFunc(s.adminOrchestratorStatus)))
s.mux.Handle("GET /admin/api/orchestrator/jobs", s.adminAuth(http.HandlerFunc(s.adminOrchestratorJobs)))
s.mux.Handle("POST /admin/api/orchestrator/jobs/{id}/retry", s.adminAuth(http.HandlerFunc(s.adminOrchestratorRetry)))
s.mux.Handle("POST /admin/api/orchestrator/jobs/{id}/cancel", s.adminAuth(http.HandlerFunc(s.adminOrchestratorCancel)))
s.mux.Handle("GET /admin/api/knowledge/events", s.adminAuth(http.HandlerFunc(s.adminKnowledgeEvents)))
s.mux.Handle("POST /admin/api/knowledge/search", s.adminAuth(http.HandlerFunc(s.adminKnowledgeSearch)))
s.mux.Handle("GET /admin/api/learning-policy", s.adminAuth(http.HandlerFunc(s.adminGetLearningPolicy)))
@@ -418,10 +428,84 @@ func (s *Server) stats(w http.ResponseWriter, r *http.Request) {
s.json(w, 200, map[string]any{"stats": s.store.Stats(), "cost": s.cost.Totals()})
}
func (s *Server) workerClaim(w http.ResponseWriter, r *http.Request) {
var q struct {
WorkerID string `json:"worker_id"`
func (s *Server) integrationGraphStatus(w http.ResponseWriter, r *http.Request) {
cfg := s.store.Config()
s.json(w, http.StatusOK, map[string]any{
"stats": s.store.GraphStats(),
"config": map[string]any{
"max_hops": cfg.Brain.GraphMaxHops, "hop_decay": cfg.Brain.GraphHopDecay,
"max_expansion": cfg.Brain.GraphMaxExpansion, "min_edge_weight": cfg.Brain.GraphMinEdgeWeight,
"backfill_enabled": cfg.Worker.GraphBackfillEnabled, "backfill_min_degree": cfg.Worker.GraphBackfillMinDegree,
},
})
}
func (s *Server) integrationOrchestratorStatus(w http.ResponseWriter, r *http.Request) {
cfg := s.store.Config()
s.json(w, http.StatusOK, map[string]any{
"leader": s.store.IsOrchestratorLeader(),
"status": s.store.OrchestratorStatus(),
"config": map[string]any{
"lease_seconds": cfg.Worker.LeaseSeconds, "heartbeat_seconds": cfg.Worker.HeartbeatSeconds,
"stale_after_seconds": cfg.Worker.StaleAfterSeconds, "max_queued_jobs": cfg.Worker.MaxQueuedJobs,
"offload_chat": cfg.Worker.OffloadChat, "offload_embeddings": cfg.Worker.OffloadEmbeddings,
},
})
}
type workerRequest struct {
WorkerID string `json:"worker_id"`
ResourceClass string `json:"resource_class,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
MaxConcurrency int `json:"max_concurrency,omitempty"`
Version string `json:"version,omitempty"`
Hostname string `json:"hostname,omitempty"`
ActiveLeases map[string]string `json:"active_leases,omitempty"`
}
func workerHeartbeatFromRequest(q workerRequest) store.WorkerHeartbeat {
return store.WorkerHeartbeat{
ID: q.WorkerID, ResourceClass: q.ResourceClass, Capabilities: q.Capabilities,
Labels: q.Labels, MaxConcurrency: q.MaxConcurrency, Version: q.Version,
Hostname: q.Hostname, ActiveLeases: q.ActiveLeases,
}
}
func (s *Server) workerRegister(w http.ResponseWriter, r *http.Request) {
var q workerRequest
if err := decode(r, &q); err != nil {
s.err(w, http.StatusBadRequest, err)
return
}
state, err := s.store.RegisterWorker(workerHeartbeatFromRequest(q))
if err != nil {
s.err(w, http.StatusBadRequest, err)
return
}
s.json(w, http.StatusOK, state)
}
func (s *Server) workerHeartbeat(w http.ResponseWriter, r *http.Request) {
var q workerRequest
if err := decode(r, &q); err != nil {
s.err(w, http.StatusBadRequest, err)
return
}
lease := s.store.Config().Worker.LeaseSeconds
if lease < 10 {
lease = 120
}
state, err := s.store.HeartbeatWorker(workerHeartbeatFromRequest(q), time.Duration(lease)*time.Second)
if err != nil {
s.err(w, http.StatusBadRequest, err)
return
}
s.json(w, http.StatusOK, state)
}
func (s *Server) workerClaim(w http.ResponseWriter, r *http.Request) {
var q workerRequest
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
@@ -434,7 +518,7 @@ func (s *Server) workerClaim(w http.ResponseWriter, r *http.Request) {
if lease < 10 {
lease = 120
}
j, err := s.store.ClaimJob(q.WorkerID, time.Duration(lease)*time.Second)
j, err := s.store.ClaimJobForWorker(workerHeartbeatFromRequest(q), time.Duration(lease)*time.Second)
if err != nil {
s.err(w, 500, err)
return
@@ -445,27 +529,33 @@ func (s *Server) workerClaim(w http.ResponseWriter, r *http.Request) {
}
s.json(w, 200, j)
}
func (s *Server) workerComplete(w http.ResponseWriter, r *http.Request) {
var q struct {
WorkerID string `json:"worker_id"`
JobID string `json:"job_id"`
Result json.RawMessage `json:"result"`
Error string `json:"error"`
WorkerID string `json:"worker_id"`
JobID string `json:"job_id"`
LeaseToken string `json:"lease_token,omitempty"`
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
j, err := s.store.CompleteJob(q.JobID, q.WorkerID, q.Result, q.Error)
j, err := s.store.CompleteJobLease(q.JobID, q.WorkerID, q.LeaseToken, q.Result, q.Error)
if err != nil {
s.err(w, 400, err)
s.err(w, 409, err)
return
}
if err := s.brain.ApplyJobResult(j); err != nil {
s.err(w, 500, err)
return
if j.Status == "apply_wait" && s.store.IsOrchestratorLeader() {
// Best-effort eager second phase. Any failure remains durable as apply_wait
// and is retried by the orchestrator; the worker must not recompute it.
_ = s.brain.ApplyAndFinalizeJob(j)
if cur, ok := s.store.Job(j.ID); ok {
j = cur
}
}
s.json(w, 200, map[string]bool{"ok": true})
s.json(w, 200, map[string]any{"ok": true, "status": j.Status, "attempts": j.Attempts, "apply_attempts": j.ApplyAttempts, "apply_error": j.ApplyError, "next_attempt_at": j.NextAttemptAt, "apply_next_attempt_at": j.ApplyNextAttemptAt})
}
func (s *Server) adminStatus(w http.ResponseWriter, r *http.Request) {