delivery, catalogue: compose-candidate takes input as a switch and answers unready for a node that cannot compose yet, so the checker decides by the verdict, never by words (ADR 0593 §7, §9)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package catalogue
|
||||
|
||||
import "testing"
|
||||
|
||||
// The node-engine's compose-candidate verb (ADR 0593 §7) is optional, asks the candidate, and takes input as a switch.
|
||||
func TestTheEngineServesComposeCandidate(t *testing.T) {
|
||||
for _, v := range EngineVerbs() {
|
||||
if v.Name != "compose-candidate" {
|
||||
continue
|
||||
}
|
||||
props, _ := v.Input["properties"].(map[string]any)
|
||||
input, _ := props["input"].(map[string]any)
|
||||
if !v.Optional || props["candidate"] == nil || input == nil || input["enum"] == nil {
|
||||
t.Fatalf("compose-candidate reads as %+v", v)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal("the seat node-engine serves no compose-candidate")
|
||||
}
|
||||
@@ -76,7 +76,9 @@ func EngineVerbs() []Verb {
|
||||
"composes (with its digest and whether the candidate changes it), is refused by the candidate or is held " +
|
||||
"without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: " +
|
||||
"applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.",
|
||||
Input: schema(map[string]string{"candidate": "the key of the indexer's candidate, <group>.<heads hash>"}, []string{"candidate"})},
|
||||
Input: schema(map[string]string{"candidate": "the key of the indexer's candidate, <group>.<heads hash>",
|
||||
"input": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path"},
|
||||
[]string{"candidate"}, "input")},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ const (
|
||||
// refused by the candidate, or at least one is (or the node could not compose with it at all, Refused).
|
||||
ComposeCandidateComposed = "composed"
|
||||
ComposeCandidateRefused = "refused"
|
||||
// ComposeCandidateUnready is the verdict of a node that cannot compose yet (its shadow not ready, its agent
|
||||
// starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer
|
||||
// (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words.
|
||||
ComposeCandidateUnready = "unready"
|
||||
|
||||
// The states of one module's composition with the candidate: composed (Digest set), refused by the candidate
|
||||
// (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict.
|
||||
@@ -47,7 +51,7 @@ type ComposeCandidateAsk struct {
|
||||
|
||||
// ComposeCandidateAnswer is the verb's answer: the node, the candidate, the verdict, every module placed on the node
|
||||
// sorted by name, the node's own composition (Own, Module "node"), why the node could not compose with the candidate at
|
||||
// all (Refused: Modules is then empty and Verdict refused), how long the engine took in milliseconds, and, when asked
|
||||
// all (Refused: Modules is then empty and Verdict refused, or unready when it cannot compose yet), how long the engine took in milliseconds, and, when asked
|
||||
// (ComposeCandidateAsk.Input), the composition input it composed from.
|
||||
type ComposeCandidateAnswer struct {
|
||||
V int `json:"v"`
|
||||
@@ -111,9 +115,13 @@ func (a ComposeCandidateAnswer) check() error {
|
||||
if a.Took < 0 {
|
||||
return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took)
|
||||
}
|
||||
if a.Verdict == ComposeCandidateUnready && a.Refused == "" {
|
||||
return fmt.Errorf("delivery: %s is not ready to compose and its answer says no why", a.Node)
|
||||
}
|
||||
if a.Refused != "" {
|
||||
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused {
|
||||
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or no refusal", a.Node, a.Refused)
|
||||
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready {
|
||||
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s nor %s",
|
||||
a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -64,6 +64,20 @@ func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) {
|
||||
if _, err := EncodeComposeCandidateAnswer(whole); err != nil {
|
||||
t.Errorf("a whole refusal was not encoded: %v", err)
|
||||
}
|
||||
unready := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateUnready, Refused: "the agent is starting"}
|
||||
if raw, err := EncodeComposeCandidateAnswer(unready); err != nil {
|
||||
t.Errorf("an unready answer was not encoded: %v", err)
|
||||
} else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateUnready {
|
||||
t.Errorf("an unready answer read back as %+v, %v", back, err)
|
||||
}
|
||||
unready.Refused = ""
|
||||
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
||||
t.Error("an unready answer saying no why was encoded")
|
||||
}
|
||||
unready.Refused, unready.Modules = "starting", good.Modules
|
||||
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
||||
t.Error("an unready answer naming modules was encoded")
|
||||
}
|
||||
if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil {
|
||||
t.Error("an answer with an unknown key was read")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user