Files
Agent-Squad/squad/blackboard.go
T
weichenandCursor 8e987bf535
ci / vet-and-test (pull_request) Failing after 1s
ci / docker-checkout-hostgateway (pull_request) Failing after 3s
Persist step approvals and guard squad HTTP entry points
Why: A step confirmation cancelled the whole run, approvals and repair budget vanished on restart, and the HTTP server accepted anonymous and oversized requests.
Evidence: GOWORK=off go vet ./... && GOWORK=off go test -count=1 ./... && GOWORK=off go test -race ./squad/... passed. scripts/submodule-consistency.sh ok.
Impact: Approve and skip apply to one step; runstate.json and unix flock survive restart; wakeup leaves permission holds pending; serve defaults to loopback and requires a token off-loopback.
Refs: Q1 Q2 Q3 Q4 Q5 Q8 Q9 Q14 Q15 Q16
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 01:48:10 +00:00

113 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package squad
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// loadBlackboard 读取黑板;不存在时返回初始黑板。
func loadBlackboard(wsDir string) (*BlackboardState, error) {
bb := &BlackboardState{Fields: map[string]string{}, UpdatedAt: time.Now()}
data, err := os.ReadFile(filepath.Join(wsDir, "blackboard.json"))
if err != nil {
if os.IsNotExist(err) {
return bb, nil
}
return nil, err
}
if err := json.Unmarshal(data, bb); err != nil {
return nil, fmt.Errorf("parse blackboard.json: %w", err)
}
if bb.Fields == nil {
bb.Fields = map[string]string{}
}
return bb, nil
}
// saveBlackboard 原子写回黑板。
func saveBlackboard(wsDir string, bb *BlackboardState) error {
bb.UpdatedAt = time.Now()
data, err := json.MarshalIndent(bb, "", " ")
if err != nil {
return err
}
return AtomicWrite(filepath.Join(wsDir, "blackboard.json"), append(data, '\n'))
}
// mergeStepArtifacts 合并步骤产出:fields.json(字符串键值)与
// gaps.json([]GapItem,角色可声明 blocking 缺口)合入黑板。
// 返回本次新合入的阻塞缺口(用于 HITL 挂起判断)。
func mergeStepArtifacts(bb *BlackboardState, subDir string) []GapItem {
var newBlocking []GapItem
if data, err := os.ReadFile(filepath.Join(subDir, "fields.json")); err == nil {
var fields map[string]string
if json.Unmarshal(data, &fields) == nil {
for k, v := range fields {
bb.Fields[k] = v
}
}
}
if data, err := os.ReadFile(filepath.Join(subDir, "gaps.json")); err == nil {
var gaps []GapItem
if json.Unmarshal(data, &gaps) == nil {
for _, g := range gaps {
if g.ID == "" {
g.ID = fmt.Sprintf("gap_%d", len(bb.Gaps)+1)
}
if exists, _ := findGap(bb, g.ID, g.Item); exists {
continue
}
if g.Owner == "" {
g.Owner = "agent"
}
bb.Gaps = append(bb.Gaps, g)
if g.Blocking && !g.Resolved {
newBlocking = append(newBlocking, g)
}
}
}
}
return newBlocking
}
func findGap(bb *BlackboardState, id, item string) (bool, bool) {
for _, g := range bb.Gaps {
if (id != "" && g.ID == id) || (item != "" && g.Item == item) {
return true, g.Resolved
}
}
return false, false
}
// UnresolvedBlockingGaps 返回未解决的阻塞缺口。
func (bb *BlackboardState) UnresolvedBlockingGaps() []GapItem {
var out []GapItem
for _, g := range bb.Gaps {
if g.Blocking && !g.Resolved {
out = append(out, g)
}
}
return out
}
// ResolveGapByID 将缺口标记为已解决,并把人工回答落成事实/字段。
func (bb *BlackboardState) ResolveGapByID(id, answer string) bool {
for i := range bb.Gaps {
if bb.Gaps[i].ID == id {
bb.Gaps[i].Resolved = true
if answer != "" {
bb.Facts = append(bb.Facts, FactItem{
ID: fmt.Sprintf("fact_%d", len(bb.Facts)+1), Claim: answer, Source: "human",
})
}
return true
}
}
return false
}