Files
foundry-core/internal/platform/canvas_ai_plan.go
T
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

223 lines
7.2 KiB
Go

package platform
import (
"encoding/json"
"errors"
"sort"
"strings"
)
type canvasAIStaticInput struct {
EdgeID string `json:"logical_edge_id"`
BoardID string `json:"board_id"`
NodeID string `json:"node_id"`
Kind string `json:"kind"`
Content map[string]any `json:"content"`
}
type canvasAIStepPlan struct {
StaticInputs []canvasAIStaticInput `json:"static_inputs"`
MaxExecutionMS int `json:"max_execution_ms"`
ID string `json:"step_id"`
BoardID string `json:"board_id"`
NodeID string `json:"node_id"`
Kind string `json:"kind"`
ModelID string `json:"model_id"`
Config map[string]any `json:"config"`
FrozenContent map[string]any `json:"frozen_content"`
Fingerprint string `json:"target_fingerprint"`
Status string `json:"status"`
Dependencies []string `json:"dependencies"`
}
type canvasAIEdgePlan struct {
ID string `json:"logical_edge_id"`
Source string `json:"source_step_id"`
Target string `json:"target_step_id"`
}
type canvasAIPlan struct {
Steps []canvasAIStepPlan `json:"steps"`
Edges []canvasAIEdgePlan `json:"edges"`
Versions map[string]int64 `json:"frozen_versions"`
}
func canvasNodeKey(board, node string) string { return board + "/" + node }
func buildCanvasAIPlan(snapshots map[string]canvasSnapshot, board, node string, propagate bool) (canvasAIPlan, error) {
plan := canvasAIPlan{Steps: []canvasAIStepPlan{}, Edges: []canvasAIEdgePlan{}, Versions: map[string]int64{}}
data := map[string]map[string]any{}
for board, snapshot := range snapshots {
plan.Versions[board] = snapshot.Version
for node, d := range canvasNodeData(snapshot.Document) {
data[canvasNodeKey(board, node)] = d
}
}
var resolve func(string, string, string, string, int) (string, error)
resolve = func(board, node, handle, direction string, depth int) (string, error) {
if depth > 8 {
return "", errors.New("graph_limit_exceeded")
}
d := data[canvasNodeKey(board, node)]
if d == nil {
return "", errGraphOwnership
}
if d["kind"] != "frame" {
if !canvasChoice(d["kind"], "text", "script", "plain-text", "image", "plain-image") {
return "", errGraphOwnership
}
return canvasNodeKey(board, node), nil
}
if !strings.HasPrefix(handle, "port:") {
return "", errGraphOwnership
}
sub, _ := d["subgraph"].(map[string]any)
child, _ := sub["boardId"].(string)
ports, _ := d["ports"].([]any)
for _, v := range ports {
p := v.(map[string]any)
if p["id"] == strings.TrimPrefix(handle, "port:") && p["direction"] == direction {
target, _ := p["nodeId"].(string)
h, _ := p["handle"].(string)
return resolve(child, target, h, direction, depth+1)
}
}
return "", errGraphOwnership
}
outgoing := map[string][]canvasAIEdgePlan{}
allIncoming := map[string][]canvasAIEdgePlan{}
for board, snapshot := range snapshots {
var doc map[string]any
_ = json.Unmarshal(snapshot.Document, &doc)
for _, v := range doc["edges"].([]any) {
e := v.(map[string]any)
d, _ := e["data"].(map[string]any)
execution, _ := d["execution"].(map[string]any)
if execution["enabled"] == false {
continue
}
source, _ := e["source"].(string)
sh, _ := e["sourceHandle"].(string)
target, _ := e["target"].(string)
th, _ := e["targetHandle"].(string)
a, err := resolve(board, source, sh, "output", 0)
if err != nil {
return plan, err
}
b, err := resolve(board, target, th, "input", 0)
if err != nil {
return plan, err
}
edge := canvasAIEdgePlan{ID: canvasNodeKey(board, e["id"].(string)), Source: a, Target: b}
outgoing[a] = append(outgoing[a], edge)
allIncoming[b] = append(allIncoming[b], edge)
}
}
root := canvasNodeKey(board, node)
if data[root] == nil || !canvasChoice(data[root]["kind"], "text", "script", "plain-text", "image", "plain-image") {
return plan, errGraphOwnership
}
for key := range outgoing {
sort.Slice(outgoing[key], func(i, j int) bool { return outgoing[key][i].ID < outgoing[key][j].ID })
}
selected := map[string]bool{}
skipped := map[string]bool{}
queue := []string{root}
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
if selected[id] {
continue
}
selected[id] = true
if len(selected) > 64 {
return plan, errGraphCapacity
}
if id != root {
ai, _ := data[id]["ai"].(map[string]any)
model, _ := ai["modelId"].(string)
if ai["enabled"] != true || model == "" {
skipped[id] = true
continue
}
}
if !propagate {
continue
}
for _, edge := range outgoing[id] {
plan.Edges = append(plan.Edges, edge)
queue = append(queue, edge.Target)
}
}
// Kahn's algorithm waits for every participating predecessor at a diamond.
indegree := map[string]int{}
incoming := map[string][]string{}
depths := map[string]int{}
for _, edge := range plan.Edges {
indegree[edge.Target]++
incoming[edge.Target] = append(incoming[edge.Target], edge.Source)
}
ready := []string{}
for id := range selected {
if indegree[id] == 0 {
ready = append(ready, id)
}
}
for len(ready) > 0 {
sort.Strings(ready)
id := ready[0]
ready = ready[1:]
parts := strings.SplitN(id, "/", 2)
d := data[id]
ai, _ := d["ai"].(map[string]any)
model, _ := ai["modelId"].(string)
status := "queued"
if skipped[id] {
status = "skipped"
}
content := canvasFrozenContent(d)
static := []canvasAIStaticInput{}
sort.Slice(allIncoming[id], func(i, j int) bool { return allIncoming[id][i].ID < allIncoming[id][j].ID })
for _, edge := range allIncoming[id] {
if propagate && selected[edge.Source] && !skipped[edge.Source] {
continue
}
origin := strings.SplitN(edge.Source, "/", 2)
source := data[edge.Source]
static = append(static, canvasAIStaticInput{EdgeID: edge.ID, BoardID: origin[0], NodeID: origin[1], Kind: source["kind"].(string), Content: canvasFrozenContent(source)})
}
dependencies := incoming[id]
if dependencies == nil {
dependencies = []string{}
}
sort.Strings(dependencies)
plan.Steps = append(plan.Steps, canvasAIStepPlan{StaticInputs: static, ID: id, BoardID: parts[0], NodeID: parts[1], Kind: d["kind"].(string), ModelID: model, Config: ai, FrozenContent: content, Fingerprint: canvasTargetFingerprint(parts[0], parts[1], d), Status: status, Dependencies: dependencies})
plan.Versions[parts[0]] = snapshots[parts[0]].Version
for _, edge := range outgoing[id] {
if !propagate || skipped[id] {
continue
}
depth := depths[id] + 1
if depth > depths[edge.Target] {
depths[edge.Target] = depth
}
if depth > 32 {
return plan, errGraphCapacity
}
indegree[edge.Target]--
if indegree[edge.Target] == 0 {
ready = append(ready, edge.Target)
}
}
}
if len(plan.Steps) != len(selected) {
return plan, errors.New("execution_cycle")
}
sort.Slice(plan.Edges, func(i, j int) bool { return plan.Edges[i].ID < plan.Edges[j].ID })
return plan, nil
}
func canvasFrozenContent(data map[string]any) map[string]any {
if canvasChoice(data["kind"], "image", "plain-image") {
return map[string]any{"src": data["src"], "assetId": data["assetId"]}
}
return map[string]any{"text": data["text"]}
}