From 52bd3d087ccd49c1c4b9df1c0d741510ce928fae Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:34:56 +0100 Subject: [PATCH] =?UTF-8?q?delivery,=20catalogue:=20compose-candidate=20ta?= =?UTF-8?q?kes=20input=20as=20a=20switch=20and=20answers=20unready=20for?= =?UTF-8?q?=20a=20node=20that=20cannot=20compose=20yet,=20so=20the=20check?= =?UTF-8?q?er=20decides=20by=20the=20verdict,=20never=20by=20words=20(ADR?= =?UTF-8?q?=200593=20=C2=A77,=20=C2=A79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/catalogue/engine_compose_candidate_test.go | 19 +++++++++++++++++++ go/catalogue/engine_seat.go | 4 +++- go/delivery/compose_candidate.go | 14 +++++++++++--- go/delivery/compose_candidate_test.go | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 go/catalogue/engine_compose_candidate_test.go diff --git a/go/catalogue/engine_compose_candidate_test.go b/go/catalogue/engine_compose_candidate_test.go new file mode 100644 index 0000000..fb34000 --- /dev/null +++ b/go/catalogue/engine_compose_candidate_test.go @@ -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") +} diff --git a/go/catalogue/engine_seat.go b/go/catalogue/engine_seat.go index 518e8e7..e5b6e3c 100644 --- a/go/catalogue/engine_seat.go +++ b/go/catalogue/engine_seat.go @@ -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, ."}, []string{"candidate"})}, + Input: schema(map[string]string{"candidate": "the key of the indexer's candidate, .", + "input": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path"}, + []string{"candidate"}, "input")}, } } diff --git a/go/delivery/compose_candidate.go b/go/delivery/compose_candidate.go index ad382a8..25d2a17 100644 --- a/go/delivery/compose_candidate.go +++ b/go/delivery/compose_candidate.go @@ -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 } diff --git a/go/delivery/compose_candidate_test.go b/go/delivery/compose_candidate_test.go index b7d069d..196ac89 100644 --- a/go/delivery/compose_candidate_test.go +++ b/go/delivery/compose_candidate_test.go @@ -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") }