Skip to content

Commit 9620caa

Browse files
committed
feat: send partial agent messages
1 parent 4ae4a6d commit 9620caa

9 files changed

Lines changed: 325 additions & 121 deletions

File tree

client/retrieval/client.go

Lines changed: 0 additions & 102 deletions
This file was deleted.

client/retrieval/connnection.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package retrieval
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"errors"
7+
"fmt"
8+
"hash"
9+
"net/http"
10+
"net/url"
11+
12+
"github.com/storacha/go-ucanto/client"
13+
"github.com/storacha/go-ucanto/core/dag/blockstore"
14+
"github.com/storacha/go-ucanto/core/delegation"
15+
"github.com/storacha/go-ucanto/core/invocation"
16+
"github.com/storacha/go-ucanto/core/ipld"
17+
"github.com/storacha/go-ucanto/core/ipld/block"
18+
"github.com/storacha/go-ucanto/core/ipld/codec/cbor"
19+
ucansha256 "github.com/storacha/go-ucanto/core/ipld/hash/sha256"
20+
"github.com/storacha/go-ucanto/core/message"
21+
mdm "github.com/storacha/go-ucanto/core/message/datamodel"
22+
"github.com/storacha/go-ucanto/core/result"
23+
rdm "github.com/storacha/go-ucanto/server/retrieval/datamodel"
24+
"github.com/storacha/go-ucanto/transport"
25+
"github.com/storacha/go-ucanto/transport/headercar"
26+
thttp "github.com/storacha/go-ucanto/transport/http"
27+
"github.com/storacha/go-ucanto/ucan"
28+
)
29+
30+
// NewConnection creates a new connection to a retrieval server that uses the
31+
// headercar transport.
32+
func NewConnection(id ucan.Principal, endpoint string) (*Connection, error) {
33+
hasher := sha256.New
34+
url, err := url.Parse(endpoint)
35+
if err != nil {
36+
return nil, err
37+
}
38+
channel := thttp.NewChannel(
39+
url,
40+
thttp.WithMethod("GET"),
41+
thttp.WithSuccessStatusCode(
42+
http.StatusOK,
43+
http.StatusPartialContent,
44+
http.StatusNotExtended, // indicates further proof must be supplied
45+
http.StatusRequestHeaderFieldsTooLarge, // indicates invocation is too large to fit in headers
46+
),
47+
)
48+
codec := headercar.NewOutboundCodec()
49+
return &Connection{id, channel, codec, hasher}, nil
50+
}
51+
52+
type Connection struct {
53+
id ucan.Principal
54+
channel transport.Channel
55+
codec transport.OutboundCodec
56+
hasher func() hash.Hash
57+
}
58+
59+
var _ client.Connection = (*Connection)(nil)
60+
61+
func (c *Connection) ID() ucan.Principal {
62+
return c.id
63+
}
64+
65+
func (c *Connection) Codec() transport.OutboundCodec {
66+
return c.codec
67+
}
68+
69+
func (c *Connection) Channel() transport.Channel {
70+
return c.channel
71+
}
72+
73+
func (c *Connection) Hasher() hash.Hash {
74+
return c.hasher()
75+
}
76+
77+
func Execute(ctx context.Context, inv invocation.Invocation, conn client.Connection) (client.ExecutionResponse, transport.HTTPResponse, error) {
78+
input, err := message.Build([]invocation.Invocation{inv}, nil)
79+
if err != nil {
80+
return nil, nil, fmt.Errorf("building message: %w", err)
81+
}
82+
83+
req, err := conn.Codec().Encode(input)
84+
if err != nil {
85+
return nil, nil, fmt.Errorf("encoding message: %w", err)
86+
}
87+
88+
response, err := conn.Channel().Request(ctx, req)
89+
if err != nil {
90+
return nil, nil, fmt.Errorf("sending message: %w", err)
91+
}
92+
93+
// if the header fields are too big, we need to split the delegation into
94+
// multiple requests...
95+
if response.Status() == http.StatusRequestHeaderFieldsTooLarge {
96+
br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(inv.Export()))
97+
if err != nil {
98+
return nil, nil, fmt.Errorf("reading invocation blocks: %w", err)
99+
}
100+
part, err := omitProofs(inv)
101+
if err != nil {
102+
return nil, nil, fmt.Errorf("creating invocation %s with omitted proofs: %w", inv.Link().String(), err)
103+
}
104+
105+
parts := map[string]delegation.Delegation{}
106+
prfs := inv.Proofs()
107+
for len(prfs) > 0 {
108+
root := prfs[0]
109+
prfs = prfs[1:]
110+
prf, err := delegation.NewDelegationView(root, br)
111+
if err != nil {
112+
return nil, nil, fmt.Errorf("creating delegation: %w", err)
113+
}
114+
prfs = append(prfs, prf.Proofs()...)
115+
// now export without proofs
116+
prf, err = omitProofs(prf)
117+
if err != nil {
118+
return nil, nil, fmt.Errorf("creating delegation %s with omitted proofs: %w", prf.Link().String(), err)
119+
}
120+
parts[prf.Link().String()] = prf
121+
}
122+
// we already tried this
123+
if len(parts) == 0 {
124+
return nil, nil, errors.New("invocation is too big to send in HTTP headers")
125+
}
126+
127+
// now send the parts
128+
for {
129+
input, err := newPartialInvocationMessage(inv.Link(), part)
130+
if err != nil {
131+
return nil, nil, fmt.Errorf("building message: %w", err)
132+
}
133+
134+
req, err := conn.Codec().Encode(input)
135+
if err != nil {
136+
return nil, nil, fmt.Errorf("encoding message: %w", err)
137+
}
138+
139+
res, err := conn.Channel().Request(ctx, req)
140+
if err != nil {
141+
return nil, nil, fmt.Errorf("sending message: %w", err)
142+
}
143+
144+
if res.Status() == http.StatusPartialContent || res.Status() == http.StatusOK {
145+
response = res
146+
break
147+
}
148+
149+
// if still too big, then fail
150+
if res.Status() == http.StatusRequestHeaderFieldsTooLarge {
151+
return nil, nil, errors.New("invocation is too big to send in HTTP headers")
152+
}
153+
154+
if res.Status() != http.StatusNotExtended {
155+
return nil, nil, fmt.Errorf("unexpected status code: %d", res.Status())
156+
}
157+
158+
// find the next part
159+
output, err := conn.Codec().Decode(response)
160+
if err != nil {
161+
return nil, nil, fmt.Errorf("decoding message: %w", err)
162+
}
163+
164+
missprfs, err := extractMissingProofs(inv.Link(), output)
165+
if err != nil {
166+
return nil, nil, fmt.Errorf("extracting missing proofs: %w", err)
167+
}
168+
169+
p, ok := parts[missprfs[0].String()]
170+
if !ok {
171+
return nil, nil, fmt.Errorf("missing proof not found or was already sent: %s", missprfs[0].String())
172+
}
173+
part = p
174+
delete(parts, p.Link().String())
175+
}
176+
}
177+
178+
output, err := conn.Codec().Decode(response)
179+
if err != nil {
180+
return nil, nil, fmt.Errorf("decoding message: %w", err)
181+
}
182+
183+
return client.ExecutionResponse(output), response, nil
184+
}
185+
186+
func omitProofs(dlg delegation.Delegation) (delegation.Delegation, error) {
187+
blocks := dlg.Export(delegation.WithOmitProof(dlg.Proofs()...))
188+
bs, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blocks))
189+
if err != nil {
190+
return nil, err
191+
}
192+
return delegation.NewDelegation(dlg.Root(), bs)
193+
}
194+
195+
func newPartialInvocationMessage(invocation ipld.Link, part delegation.Delegation) (message.AgentMessage, error) {
196+
bs, err := blockstore.NewBlockStore(blockstore.WithBlocksIterator(part.Export()))
197+
if err != nil {
198+
return nil, err
199+
}
200+
msg := mdm.AgentMessageModel{
201+
UcantoMessage7: &mdm.DataModel{
202+
Execute: []ipld.Link{invocation},
203+
},
204+
}
205+
rt, err := block.Encode(
206+
&msg,
207+
mdm.Type(),
208+
cbor.Codec,
209+
ucansha256.Hasher,
210+
)
211+
if err != nil {
212+
return nil, err
213+
}
214+
err = bs.Put(rt)
215+
if err != nil {
216+
return nil, err
217+
}
218+
return message.NewMessage(rt.Link(), bs)
219+
}
220+
221+
func extractMissingProofs(invocation ipld.Link, msg message.AgentMessage) ([]ipld.Link, error) {
222+
root, ok := msg.Get(invocation)
223+
if !ok {
224+
return nil, fmt.Errorf("missing receipt for invocation: %s", invocation.String())
225+
}
226+
rcpt, ok, err := msg.Receipt(root)
227+
if err != nil {
228+
return nil, fmt.Errorf("getting receipt %s: %w", root.String(), err)
229+
}
230+
if !ok {
231+
return nil, fmt.Errorf("missing receipt for invocation: %s", invocation.String())
232+
}
233+
_, x := result.Unwrap(rcpt.Out())
234+
if x == nil {
235+
return nil, errors.New("missing error in receipt")
236+
}
237+
model, err := ipld.Rebind[rdm.MissingProofsModel](x, rdm.MissingProofsType())
238+
if err != nil {
239+
return nil, fmt.Errorf("binding missing proofs error: %w", err)
240+
}
241+
return model.Proofs, nil
242+
}

0 commit comments

Comments
 (0)