Files
foundry-core/internal/platform/canvas_ai_query.go
T
codex-bot f12c4ad8bd
Foundry Go checks / check (push) Successful in 2m51s
Foundry Go checks / check (pull_request) Successful in 3m6s
Persist queryable AI action outcomes and zero-effect rejection receipts
2026-11-08 00:53:11 +08:00

203 lines
6.5 KiB
Go

package platform
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
)
func publicAIPlan(plan canvasAIPlan) response {
steps := []any{}
for _, step := range plan.Steps {
static := []any{}
for _, source := range step.StaticInputs {
static = append(static, response{"logical_edge_id": source.EdgeID, "board_id": source.BoardID, "node_id": source.NodeID, "kind": source.Kind, "source": "frozen_snapshot"})
}
steps = append(steps, response{"step_id": step.ID, "board_id": step.BoardID, "node_id": step.NodeID, "target_incarnation": step.TargetIncarnation, "canonical_root": step.CanonicalRoot, "kind": step.Kind, "model_id": step.ModelID, "max_execution_ms": step.MaxExecutionMS, "status": step.Status, "dependencies": step.Dependencies, "static_inputs": static})
}
return response{"steps": steps, "edges": plan.Edges, "frozen_versions": plan.Versions}
}
func (s *service) getCanvasAIRun(w http.ResponseWriter, r *http.Request, project, account string) error {
if r.Method != http.MethodGet {
reject(w, 405, "method_not_allowed")
return nil
}
id := r.PathValue("run")
if !uuidSlugPattern.MatchString(id) {
reject(w, 404, "not_found")
return nil
}
after := int64(0)
limit := 100
var err error
if raw := r.URL.Query().Get("after_seq"); raw != "" {
after, err = strconv.ParseInt(raw, 10, 64)
}
if err != nil || after < 0 {
reject(w, 400, "invalid_pagination")
return nil
}
if raw := r.URL.Query().Get("limit"); raw != "" {
limit, err = strconv.Atoi(raw)
}
if err != nil || limit < 1 || limit > 100 {
reject(w, 400, "invalid_pagination")
return nil
}
workspace, board := r.PathValue("workspace"), r.PathValue("material")
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, _, err = lockMaterial(r.Context(), tx, project, workspace, board, account); err != nil {
return err
}
var status string
var rawPlan json.RawMessage
var last int64
var errorCode sql.NullString
err = tx.QueryRowContext(r.Context(), `SELECT status,plan,last_seq,error_code FROM canvas_ai_runs WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND id=$4`, project, workspace, board, id).Scan(&status, &rawPlan, &last, &errorCode)
if err != nil {
return err
}
var plan canvasAIPlan
if err = json.Unmarshal(rawPlan, &plan); err != nil {
return err
}
checked := map[string]bool{board: true}
for _, step := range plan.Steps {
if !checked[step.BoardID] {
if _, _, err = lockMaterial(r.Context(), tx, project, workspace, step.BoardID, account); err != nil {
return err
}
checked[step.BoardID] = true
}
}
rows, err := tx.QueryContext(r.Context(), `SELECT id,board_id::text,node_id,status,error_code,applied_version,CASE WHEN output IS NULL THEN NULL ELSE output-'text' END FROM canvas_ai_steps WHERE project=$1 AND workspace_id=$2 AND run_id=$3 ORDER BY ordinal`, project, workspace, id)
if err != nil {
return err
}
steps := []any{}
outputs := []any{}
versions := map[string]int64{}
for rows.Next() {
var step, node, stepBoard, stepStatus string
var code sql.NullString
var version sql.NullInt64
var output []byte
if err = rows.Scan(&step, &stepBoard, &node, &stepStatus, &code, &version, &output); err != nil {
rows.Close()
return err
}
item := response{"step_id": step, "board_id": stepBoard, "node_id": node, "status": stepStatus}
if code.Valid {
item["error"] = code.String
}
if version.Valid {
item["applied_version"] = version.Int64
if version.Int64 > versions[stepBoard] {
versions[stepBoard] = version.Int64
}
}
if len(output) > 0 {
outputs = append(outputs, response{"step_id": step, "output": json.RawMessage(output)})
}
steps = append(steps, item)
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
rows, err = tx.QueryContext(r.Context(), `SELECT seq,type,data,created_at FROM canvas_ai_events WHERE project=$1 AND workspace_id=$2 AND run_id=$3 AND seq>$4 ORDER BY seq LIMIT $5`, project, workspace, id, after, limit)
if err != nil {
return err
}
events := []any{}
next := after
size := 0
for rows.Next() {
var seq int64
var kind string
var data json.RawMessage
var timestamp any
if err = rows.Scan(&seq, &kind, &data, &timestamp); err != nil {
rows.Close()
return err
}
if size+len(data) > 800<<10 && len(events) > 0 {
break
}
var event response
if err = json.Unmarshal(data, &event); err != nil {
rows.Close()
return err
}
if event == nil {
event = response{}
}
event["seq"] = seq
event["run_id"] = id
event["type"] = kind
event["timestamp"] = timestamp
events = append(events, event)
next = seq
size += len(data)
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
result := response{"run_id": id, "status": status, "plan": publicAIPlan(plan), "steps": steps, "events": events, "next_seq": next, "has_more": next < last, "outputs": outputs, "applied_versions": versions, "retention_days": 30}
if errorCode.Valid {
result["error"] = errorCode.String
}
reply(w, 200, result)
return nil
}
// Resolve admission by the original client key, before the caller knows a run ID.
// Absence is unknown, not a terminal proof that an earlier invocation had no effect.
func (s *service) getCanvasAIAdmission(w http.ResponseWriter, r *http.Request, project, account string) error {
if r.Method != http.MethodGet {
reject(w, 405, "method_not_allowed")
return nil
}
key := r.PathValue("request")
if !uuidSlugPattern.MatchString(key) {
reject(w, 400, "invalid_request_id")
return nil
}
workspace, board := r.PathValue("workspace"), r.PathValue("material")
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, _, err = lockMaterial(r.Context(), tx, project, workspace, board, account); err != nil {
return err
}
var body json.RawMessage
err = tx.QueryRowContext(r.Context(), `SELECT response_body FROM canvas_ai_requests WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND account_id=$4 AND request_id=$5`, project, workspace, board, account, key).Scan(&body)
if err == nil {
reply(w, 200, body)
return nil
}
if err != sql.ErrNoRows {
return err
}
err = tx.QueryRowContext(r.Context(), `SELECT create_response FROM canvas_ai_runs WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND account_id=$4 AND idempotency_key=$5`, project, workspace, board, account, key).Scan(&body)
if err == sql.ErrNoRows {
reply(w, 404, response{"protocol_version": 2, "request_id": key, "status": "unknown", "error": "ai_admission_unknown"})
return nil
}
if err != nil {
return err
}
reply(w, 200, body)
return nil
}