buscalls.Unwrap: one reading of a tool's answer whatever wraps it
A JSON string, the content list, {output, ok, answer} or the tool runner's {result, error}: mesh-config's
unwrapAnswer and the distribution's copy take it, and views.AskVia reads through it.
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package buscalls
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Unwrap reads a tool's answer whatever wraps it on its way from the tool runner (the lead's ruling 2026-11-04 13:12:
|
||||
// one reading, not a copy per module): a JSON string holding the answer; the protocol's content list, whose first
|
||||
// text is the answer and whose isError says it failed; the {output, ok, answer} form; the tool runner's {result,
|
||||
// error} envelope, whose error is the call's. It answers the answer itself, the words said with it (the output, the
|
||||
// error, or the answer's own text), and whether the tool says it succeeded. A bare answer is itself, succeeded.
|
||||
func Unwrap(raw json.RawMessage) (answer json.RawMessage, said string, ok bool) {
|
||||
var s string
|
||||
if json.Unmarshal(raw, &s) == nil {
|
||||
return Unwrap(json.RawMessage(s))
|
||||
}
|
||||
var m map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &m) != nil {
|
||||
return raw, string(raw), true
|
||||
}
|
||||
if content, has := m["content"]; has {
|
||||
var items []struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
var e struct {
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &e)
|
||||
if json.Unmarshal(content, &items) == nil && len(items) > 0 {
|
||||
inner, out, ok := Unwrap(json.RawMessage(items[0].Text))
|
||||
return inner, out, ok && !e.IsError
|
||||
}
|
||||
}
|
||||
var v struct {
|
||||
Output string `json:"output"`
|
||||
OK *bool `json:"ok"`
|
||||
Answer json.RawMessage `json:"answer"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(raw, &v) == nil {
|
||||
switch {
|
||||
case v.OK != nil:
|
||||
return v.Answer, v.Output, *v.OK
|
||||
case v.Error != "":
|
||||
return nil, v.Error, false
|
||||
case len(v.Result) > 0:
|
||||
return Unwrap(v.Result)
|
||||
}
|
||||
}
|
||||
return raw, string(raw), true
|
||||
}
|
||||
@@ -192,6 +192,25 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
|
||||
if string(entry) != `{"instance":"listing/a17","sequence":3}` {
|
||||
t.Errorf("an index entry is %s", entry)
|
||||
}
|
||||
// A tool's answer reads the same whatever wraps it (buscalls.Unwrap, the lead's ruling 2026-11-04 13:12).
|
||||
for _, c := range []struct {
|
||||
name, raw, answer, said string
|
||||
ok bool
|
||||
}{
|
||||
{"bare", `{"a":1}`, `{"a":1}`, `{"a":1}`, true},
|
||||
{"a JSON string", `"{\"a\":1}"`, `{"a":1}`, `{"a":1}`, true},
|
||||
{"the content list", `{"content":[{"type":"text","text":"{\"a\":1}"}]}`, `{"a":1}`, `{"a":1}`, true},
|
||||
{"the content list, failed", `{"content":[{"type":"text","text":"no"}],"isError":true}`, `no`, `no`, false},
|
||||
{"output, ok, answer", `{"output":"done","ok":true,"answer":{"a":1}}`, `{"a":1}`, "done", true},
|
||||
{"output, not ok", `{"output":"refused","ok":false}`, ``, "refused", false},
|
||||
{"the runner's result", `{"result":{"a":1}}`, `{"a":1}`, `{"a":1}`, true},
|
||||
{"the runner's error", `{"error":"no such tool"}`, ``, "no such tool", false},
|
||||
} {
|
||||
answer, said, ok := buscalls.Unwrap(json.RawMessage(c.raw))
|
||||
if string(answer) != c.answer || said != c.said || ok != c.ok {
|
||||
t.Errorf("%s: unwrapped %s, %q, %v; want %s, %q, %v", c.name, answer, said, ok, c.answer, c.said, c.ok)
|
||||
}
|
||||
}
|
||||
// 1966/AC5: an apply naming the tool call it serves, and its answer with the state behind, round-trip.
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
|
||||
+7
-10
@@ -11,6 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
)
|
||||
|
||||
// Follower is a reader's picture of one state of an owner (ADR 0615 §4 to §6): it asks the owner's first-view tool at
|
||||
@@ -353,7 +355,7 @@ func (f *Follower) Gap(ctx context.Context, why string) error {
|
||||
}
|
||||
|
||||
// AskVia is a Follower's Ask through a call of the owner's tool (process.Conn.Call, or stdio.Ask through the tool
|
||||
// runner), decoding its answer: bare, or in the tool runner's envelope {result, error}, whose error is the ask's.
|
||||
// runner), decoding its answer whatever wraps it (buscalls.Unwrap): an error the tool says is the ask's.
|
||||
func AskVia(call func(key string, body any) (json.RawMessage, error), tool string) func(context.Context, string) (Answer, error) {
|
||||
return func(_ context.Context, after string) (Answer, error) {
|
||||
body := map[string]any{}
|
||||
@@ -364,16 +366,11 @@ func AskVia(call func(key string, body any) (json.RawMessage, error), tool strin
|
||||
if err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
var wrapped struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(raw, &wrapped) == nil && (len(wrapped.Result) > 0 || wrapped.Error != "") {
|
||||
if wrapped.Error != "" {
|
||||
return Answer{}, fmt.Errorf("%s answered: %s", tool, wrapped.Error)
|
||||
}
|
||||
raw = wrapped.Result
|
||||
answer, said, ok := buscalls.Unwrap(raw)
|
||||
if !ok {
|
||||
return Answer{}, fmt.Errorf("%s answered: %s", tool, said)
|
||||
}
|
||||
raw = answer
|
||||
return DecodeAnswer(raw)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user