@pi-in-go/pigpen-a2a 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CREDITS.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/extensions/a2a/auth.go +120 -0
  5. package/extensions/a2a/auth_test.go +133 -0
  6. package/extensions/a2a/bench_test.go +70 -0
  7. package/extensions/a2a/binary_test.go +225 -0
  8. package/extensions/a2a/cancelqueued_test.go +142 -0
  9. package/extensions/a2a/card_cache_test.go +42 -0
  10. package/extensions/a2a/client.go +499 -0
  11. package/extensions/a2a/client_test.go +402 -0
  12. package/extensions/a2a/config.go +282 -0
  13. package/extensions/a2a/config_test.go +194 -0
  14. package/extensions/a2a/e2e_test.go +311 -0
  15. package/extensions/a2a/executor.go +318 -0
  16. package/extensions/a2a/extension.go +382 -0
  17. package/extensions/a2a/extension_test.go +396 -0
  18. package/extensions/a2a/fakehost_test.go +548 -0
  19. package/extensions/a2a/fakepig_test.go +212 -0
  20. package/extensions/a2a/gaps_test.go +59 -0
  21. package/extensions/a2a/go.mod +15 -0
  22. package/extensions/a2a/go.sum +14 -0
  23. package/extensions/a2a/interop_test.go +163 -0
  24. package/extensions/a2a/procattr_other.go +25 -0
  25. package/extensions/a2a/procattr_windows.go +26 -0
  26. package/extensions/a2a/resubscribe_test.go +113 -0
  27. package/extensions/a2a/review_test.go +293 -0
  28. package/extensions/a2a/server.go +267 -0
  29. package/extensions/a2a/server_test.go +792 -0
  30. package/extensions/a2a/survivors_test.go +302 -0
  31. package/extensions/a2a/worker.go +388 -0
  32. package/extensions/a2a/worker_test.go +257 -0
  33. package/package.json +41 -0
  34. package/port/PORT.md +126 -0
  35. package/port/a2a-go-LICENSE +201 -0
  36. package/port/golden/flag-without-auth-refused.jsonl +5 -0
  37. package/port/golden/listener-off.jsonl +4 -0
  38. package/port/golden/send-missing-message.jsonl +18 -0
  39. package/port/golden/send-without-remotes.jsonl +18 -0
  40. package/port/golden/task-unknown-action.jsonl +18 -0
  41. package/port/golden/tools-visible-to-model.jsonl +11 -0
  42. package/port/interop/kagent/main.go +73 -0
  43. package/port/mutation-run.txt +222 -0
  44. package/port/mutations.json +656 -0
  45. package/port/red.txt +105 -0
  46. package/port/scenarios/flag-without-auth-refused.json +8 -0
  47. package/port/scenarios/listener-off.json +7 -0
  48. package/port/scenarios/send-missing-message.json +11 -0
  49. package/port/scenarios/send-without-remotes.json +11 -0
  50. package/port/scenarios/task-unknown-action.json +11 -0
  51. package/port/scenarios/tools-visible-to-model.json +8 -0
  52. package/provenance.json +18 -0
@@ -0,0 +1,499 @@
1
+ package a2aext
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "net/http"
8
+ "net/url"
9
+ "sort"
10
+ "strings"
11
+ "sync"
12
+ "time"
13
+
14
+ "github.com/a2aproject/a2a-go/v2/a2a"
15
+ "github.com/a2aproject/a2a-go/v2/a2aclient"
16
+ "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard"
17
+ )
18
+
19
+ // Remotes calls configured A2A agents over protocol 1.0 (JSON-RPC binding).
20
+ type Remotes struct {
21
+ cfg map[string]RemoteAgent
22
+ getenv func(string) string
23
+
24
+ mu sync.Mutex
25
+ clients map[string]*a2aclient.Client
26
+ cards map[string]*a2a.AgentCard
27
+ }
28
+
29
+ // SendArgs are the inputs of Send.
30
+ type SendArgs struct {
31
+ Agent, Message, ContextID, TaskID string
32
+ // OnTask, if set, is called once, as soon as the remote reports the task's ids.
33
+ OnTask func(taskID, contextID string)
34
+ }
35
+
36
+ // TaskSummary is what a remote task looks like to the model.
37
+ type TaskSummary struct {
38
+ TaskID string
39
+ ContextID string
40
+ // State is lower-case with hyphens: submitted, working, input-required, completed, canceled, failed, rejected, auth-required.
41
+ State string
42
+ Text string
43
+ Terminal bool
44
+ }
45
+
46
+ // NewRemotes builds the client set. Nothing is dialled until a call is made.
47
+ func NewRemotes(cfg map[string]RemoteAgent, getenv func(string) string) *Remotes {
48
+ if getenv == nil {
49
+ getenv = func(string) string { return "" }
50
+ }
51
+ withDefaults := make(map[string]RemoteAgent, len(cfg))
52
+ for n, ra := range cfg {
53
+ if ra.TimeoutSeconds <= 0 {
54
+ ra.TimeoutSeconds = defaultRemoteTimeout
55
+ }
56
+ withDefaults[n] = ra
57
+ }
58
+ cfg = withDefaults
59
+ return &Remotes{cfg: cfg, getenv: getenv, clients: map[string]*a2aclient.Client{}, cards: map[string]*a2a.AgentCard{}}
60
+ }
61
+
62
+ // Names lists configured remote names in order.
63
+ func (r *Remotes) Names() []string {
64
+ names := make([]string, 0, len(r.cfg))
65
+ for n := range r.cfg {
66
+ names = append(names, n)
67
+ }
68
+ sort.Strings(names)
69
+ return names
70
+ }
71
+
72
+ func (r *Remotes) remote(name string) (RemoteAgent, error) {
73
+ ra, ok := r.cfg[name]
74
+ if !ok {
75
+ if len(r.cfg) == 0 {
76
+ return RemoteAgent{}, errors.New("a2a: no remotes are configured (add a \"remotes\" object to a2a.json)")
77
+ }
78
+ return RemoteAgent{}, fmt.Errorf("a2a: unknown agent %q; configured remotes: %s", name, strings.Join(r.Names(), ", "))
79
+ }
80
+ return ra, nil
81
+ }
82
+
83
+ func (ra RemoteAgent) hasCredentials() bool { return ra.BearerTokenEnv != "" || len(ra.HeaderEnv) > 0 }
84
+
85
+ func (r *Remotes) headers(name string, ra RemoteAgent) (http.Header, error) {
86
+ h := http.Header{}
87
+ if ra.BearerTokenEnv != "" {
88
+ v := r.getenv(ra.BearerTokenEnv)
89
+ if v == "" {
90
+ return nil, fmt.Errorf("a2a: remote %q needs the environment variable %s (bearerTokenEnv), which is not set", name, ra.BearerTokenEnv)
91
+ }
92
+ h.Set("Authorization", "Bearer "+v)
93
+ }
94
+ for header, env := range ra.HeaderEnv {
95
+ v := r.getenv(env)
96
+ if v == "" {
97
+ return nil, fmt.Errorf("a2a: remote %q needs the environment variable %s (headerEnv %s), which is not set", name, env, header)
98
+ }
99
+ h.Set(header, v)
100
+ }
101
+ return h, nil
102
+ }
103
+
104
+ type headerTransport struct {
105
+ h http.Header
106
+ host string // credentials go to this host only
107
+ scheme string // and over this scheme only (an https remote's token never goes over http)
108
+ base http.RoundTripper
109
+ }
110
+
111
+ func (t headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
112
+ req = req.Clone(req.Context())
113
+ if req.URL.Host != t.host {
114
+ return nil, fmt.Errorf("a2a: refusing to send credentials to %s (the remote is configured at %s)", req.URL.Host, t.host)
115
+ }
116
+ if req.URL.Scheme != t.scheme {
117
+ return nil, fmt.Errorf("a2a: refusing to send credentials over %s (the remote is configured with %s)", req.URL.Scheme, t.scheme)
118
+ }
119
+ for k, v := range t.h {
120
+ req.Header[k] = v
121
+ }
122
+ return t.base.RoundTrip(req)
123
+ }
124
+
125
+ func hostOf(raw string) string {
126
+ u, err := url.Parse(raw)
127
+ if err != nil {
128
+ return ""
129
+ }
130
+ return u.Host
131
+ }
132
+
133
+ func schemeOf(raw string) string {
134
+ u, err := url.Parse(raw)
135
+ if err != nil {
136
+ return ""
137
+ }
138
+ return u.Scheme
139
+ }
140
+
141
+ // originOf is scheme://host, the unit credentials are pinned to.
142
+ func originOf(raw string) string { return schemeOf(raw) + "://" + hostOf(raw) }
143
+
144
+ func (r *Remotes) httpClient(name string, ra RemoteAgent) (*http.Client, error) {
145
+ h, err := r.headers(name, ra)
146
+ if err != nil {
147
+ return nil, err
148
+ }
149
+ var rt http.RoundTripper = http.DefaultTransport
150
+ if len(h) > 0 {
151
+ rt = headerTransport{h: h, host: hostOf(ra.URL), scheme: schemeOf(ra.URL), base: http.DefaultTransport}
152
+ }
153
+ return &http.Client{Transport: rt, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, nil
154
+ }
155
+
156
+ func (r *Remotes) call(ctx context.Context, ra RemoteAgent) (context.Context, context.CancelFunc) {
157
+ return context.WithTimeout(ctx, time.Duration(ra.TimeoutSeconds)*time.Second)
158
+ }
159
+
160
+ // Card resolves a remote's agent card.
161
+ func (r *Remotes) Card(ctx context.Context, name string) (*a2a.AgentCard, error) {
162
+ ra, err := r.remote(name)
163
+ if err != nil {
164
+ return nil, err
165
+ }
166
+ if ra.SkipCard {
167
+ return nil, fmt.Errorf("a2a: remote %q is configured with skipCard; it has no card to show", name)
168
+ }
169
+ return r.card(ctx, name, ra)
170
+ }
171
+
172
+ func (r *Remotes) card(ctx context.Context, name string, ra RemoteAgent) (*a2a.AgentCard, error) {
173
+ r.mu.Lock()
174
+ if c := r.cards[name]; c != nil {
175
+ r.mu.Unlock()
176
+ return c, nil
177
+ }
178
+ r.mu.Unlock()
179
+ hc, err := r.httpClient(name, ra)
180
+ if err != nil {
181
+ return nil, err
182
+ }
183
+ ctx, cancel := r.call(ctx, ra)
184
+ defer cancel()
185
+ card, err := agentcard.NewResolver(hc).Resolve(ctx, ra.URL)
186
+ if err != nil {
187
+ return nil, fmt.Errorf("a2a: fetch the agent card of %q: %w", name, err)
188
+ }
189
+ r.mu.Lock()
190
+ r.cards[name] = card
191
+ r.mu.Unlock()
192
+ return card, nil
193
+ }
194
+
195
+ func (r *Remotes) client(ctx context.Context, name string, ra RemoteAgent) (*a2aclient.Client, error) {
196
+ r.mu.Lock()
197
+ if c := r.clients[name]; c != nil {
198
+ r.mu.Unlock()
199
+ return c, nil
200
+ }
201
+ r.mu.Unlock()
202
+ hc, err := r.httpClient(name, ra)
203
+ if err != nil {
204
+ return nil, err
205
+ }
206
+ // Defaults are disabled so nothing but the pinned JSON-RPC 1.0 transport can be selected.
207
+ opts := []a2aclient.FactoryOption{a2aclient.WithDefaultsDisabled(), a2aclient.WithJSONRPCTransport(hc)}
208
+ var c *a2aclient.Client
209
+ if ra.SkipCard {
210
+ c, err = a2aclient.NewFromEndpoints(ctx, []*a2a.AgentInterface{a2a.NewAgentInterface(ra.URL, a2a.TransportProtocolJSONRPC)}, opts...)
211
+ } else {
212
+ card, cerr := r.card(ctx, name, ra)
213
+ if cerr != nil {
214
+ return nil, cerr
215
+ }
216
+ iface, ierr := pinnedInterface(name, ra, card)
217
+ if ierr != nil {
218
+ return nil, ierr
219
+ }
220
+ c, err = a2aclient.NewFromEndpoints(ctx, []*a2a.AgentInterface{iface}, opts...)
221
+ }
222
+ if err != nil {
223
+ return nil, fmt.Errorf("a2a: connect to %q: %w", name, err)
224
+ }
225
+ r.mu.Lock()
226
+ r.clients[name] = c
227
+ r.mu.Unlock()
228
+ return c, nil
229
+ }
230
+
231
+ // pinnedInterface picks the card's A2A 1.0 JSON-RPC interface. Credentials only go to the
232
+ // configured origin (scheme and host): a card is not trusted to redirect or downgrade them.
233
+ func pinnedInterface(name string, ra RemoteAgent, card *a2a.AgentCard) (*a2a.AgentInterface, error) {
234
+ var offered []string
235
+ for _, i := range card.SupportedInterfaces {
236
+ offered = append(offered, string(i.ProtocolBinding)+" "+string(i.ProtocolVersion))
237
+ if i.ProtocolBinding != a2a.TransportProtocolJSONRPC || !versionOK(string(i.ProtocolVersion)) {
238
+ continue
239
+ }
240
+ if ra.hasCredentials() && originOf(i.URL) != originOf(ra.URL) {
241
+ return nil, fmt.Errorf("a2a: the card of %q names %s, not the configured %s; not sending credentials there (set skipCard to use the configured URL as the endpoint)", name, originOf(i.URL), originOf(ra.URL))
242
+ }
243
+ return i, nil
244
+ }
245
+ return nil, fmt.Errorf("a2a: %q offers no A2A %s JSON-RPC interface (offers: %s); this adapter is pinned to protocol %s", name, ProtocolVersion, strings.Join(offered, ", "), ProtocolVersion)
246
+ }
247
+
248
+ func stateName(s a2a.TaskState) string {
249
+ n := strings.TrimPrefix(string(s), "TASK_STATE_")
250
+ return strings.ToLower(strings.ReplaceAll(n, "_", "-"))
251
+ }
252
+
253
+ func textOf(parts a2a.ContentParts) string {
254
+ var b strings.Builder
255
+ for _, p := range parts {
256
+ if t, ok := p.Content.(a2a.Text); ok {
257
+ b.WriteString(string(t))
258
+ }
259
+ }
260
+ return b.String()
261
+ }
262
+
263
+ func messageText(m *a2a.Message) string {
264
+ if m == nil {
265
+ return ""
266
+ }
267
+ return textOf(m.Parts)
268
+ }
269
+
270
+ func summarize(t *a2a.Task) TaskSummary {
271
+ var arts []string
272
+ for _, a := range t.Artifacts {
273
+ if s := textOf(a.Parts); s != "" {
274
+ arts = append(arts, s)
275
+ }
276
+ }
277
+ return finish(TaskSummary{TaskID: string(t.ID), ContextID: t.ContextID, State: stateName(t.Status.State), Terminal: t.Status.State.Terminal()},
278
+ strings.Join(arts, "\n\n"), messageText(t.Status.Message), t.Status.State)
279
+ }
280
+
281
+ func finish(s TaskSummary, artifactText, statusText string, state a2a.TaskState) TaskSummary {
282
+ var parts []string
283
+ if artifactText != "" {
284
+ parts = append(parts, artifactText)
285
+ }
286
+ if statusText != "" && state != a2a.TaskStateCompleted {
287
+ parts = append(parts, statusText)
288
+ }
289
+ s.Text = strings.Join(parts, "\n\n")
290
+ return s
291
+ }
292
+
293
+ // Send sends a message and returns when the task is terminal or needs input. onProgress
294
+ // receives assistant text as it streams. Cancelling ctx (or the timeout) cancels the remote task.
295
+ func (r *Remotes) Send(ctx context.Context, args SendArgs, onProgress func(string)) (TaskSummary, error) {
296
+ ra, err := r.remote(args.Agent)
297
+ if err != nil {
298
+ return TaskSummary{}, err
299
+ }
300
+ if strings.TrimSpace(args.Message) == "" {
301
+ return TaskSummary{}, errors.New("a2a: message is empty")
302
+ }
303
+ callCtx, cancelCall := r.call(ctx, ra)
304
+ defer cancelCall()
305
+ c, err := r.client(callCtx, args.Agent, ra)
306
+ if err != nil {
307
+ return TaskSummary{}, err
308
+ }
309
+ msg := a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(args.Message))
310
+ msg.ContextID, msg.TaskID = args.ContextID, a2a.TaskID(args.TaskID)
311
+ req := &a2a.SendMessageRequest{Message: msg}
312
+
313
+ streaming := ra.SkipCard
314
+ if !ra.SkipCard {
315
+ if card, err := r.card(callCtx, args.Agent, ra); err == nil {
316
+ streaming = card.Capabilities.Streaming
317
+ }
318
+ }
319
+ var events func(func(a2a.Event, error) bool)
320
+ if streaming {
321
+ events = c.SendStreamingMessage(callCtx, req)
322
+ } else {
323
+ events = func(yield func(a2a.Event, error) bool) {
324
+ res, err := c.SendMessage(callCtx, req)
325
+ if err != nil {
326
+ yield(nil, err)
327
+ return
328
+ }
329
+ yield(res, nil)
330
+ }
331
+ }
332
+
333
+ var (
334
+ sum TaskSummary
335
+ artifacts []a2a.ArtifactID
336
+ artText = map[a2a.ArtifactID]*strings.Builder{}
337
+ statusText string
338
+ state a2a.TaskState
339
+ final *TaskSummary
340
+ )
341
+ assembled := func() string {
342
+ var parts []string
343
+ for _, id := range artifacts {
344
+ if s := artText[id].String(); s != "" {
345
+ parts = append(parts, s)
346
+ }
347
+ }
348
+ return strings.Join(parts, "\n\n")
349
+ }
350
+ announced := false
351
+ for ev, err := range events {
352
+ if err != nil {
353
+ return r.failedSend(ctx, callCtx, args.Agent, ra, sum, err)
354
+ }
355
+ switch e := ev.(type) {
356
+ case *a2a.Task:
357
+ s := summarize(e)
358
+ final = &s
359
+ sum.TaskID, sum.ContextID = s.TaskID, s.ContextID
360
+ state, statusText = e.Status.State, messageText(e.Status.Message)
361
+ for _, a := range e.Artifacts {
362
+ if artText[a.ID] == nil {
363
+ artText[a.ID] = &strings.Builder{}
364
+ artifacts = append(artifacts, a.ID)
365
+ }
366
+ artText[a.ID].Reset()
367
+ artText[a.ID].WriteString(textOf(a.Parts))
368
+ }
369
+ case *a2a.Message:
370
+ s := TaskSummary{TaskID: string(e.TaskID), ContextID: e.ContextID, State: "message", Text: messageText(e), Terminal: true}
371
+ return s, nil
372
+ case *a2a.TaskStatusUpdateEvent:
373
+ sum.TaskID, sum.ContextID = string(e.TaskID), e.ContextID
374
+ state, statusText = e.Status.State, messageText(e.Status.Message)
375
+ final = nil
376
+ case *a2a.TaskArtifactUpdateEvent:
377
+ sum.TaskID, sum.ContextID = string(e.TaskID), e.ContextID
378
+ id := e.Artifact.ID
379
+ if artText[id] == nil {
380
+ artText[id] = &strings.Builder{}
381
+ artifacts = append(artifacts, id)
382
+ }
383
+ if !e.Append {
384
+ artText[id].Reset()
385
+ }
386
+ chunk := textOf(e.Artifact.Parts)
387
+ artText[id].WriteString(chunk)
388
+ if chunk != "" && onProgress != nil {
389
+ onProgress(chunk)
390
+ }
391
+ final = nil
392
+ }
393
+ if !announced && sum.TaskID != "" {
394
+ announced = true
395
+ if args.OnTask != nil {
396
+ args.OnTask(sum.TaskID, sum.ContextID)
397
+ }
398
+ }
399
+ }
400
+ if final != nil {
401
+ return *final, nil
402
+ }
403
+ if state == a2a.TaskStateUnspecified {
404
+ return sum, fmt.Errorf("a2a: %q ended the stream without a task state", args.Agent)
405
+ }
406
+ sum.State, sum.Terminal = stateName(state), state.Terminal()
407
+ return finish(sum, assembled(), statusText, state), nil
408
+ }
409
+
410
+ // failedSend reports a broken or cancelled call and, when a remote task was started and is
411
+ // still going, asks the remote to cancel it.
412
+ func (r *Remotes) failedSend(caller, call context.Context, name string, ra RemoteAgent, sum TaskSummary, err error) (TaskSummary, error) {
413
+ if sum.TaskID != "" && (caller.Err() != nil || call.Err() != nil) {
414
+ cctx, cancel := context.WithTimeout(context.WithoutCancel(caller), 10*time.Second)
415
+ defer cancel()
416
+ if c, cerr := r.client(cctx, name, ra); cerr == nil {
417
+ _, _ = c.CancelTask(cctx, &a2a.CancelTaskRequest{ID: a2a.TaskID(sum.TaskID)})
418
+ }
419
+ }
420
+ switch {
421
+ case caller.Err() != nil:
422
+ return sum, caller.Err()
423
+ case call.Err() != nil:
424
+ return sum, fmt.Errorf("a2a: %q did not finish within %ds: %w", name, ra.TimeoutSeconds, call.Err())
425
+ }
426
+ return sum, fmt.Errorf("a2a: %q: %w", name, describe(err))
427
+ }
428
+
429
+ // describe keeps the protocol error but never a credential: errors from the transport
430
+ // carry status lines and bodies, not request headers.
431
+ func describe(err error) error {
432
+ switch {
433
+ case errors.Is(err, a2a.ErrUnauthenticated):
434
+ return errors.New("authentication failed (the remote rejected the credentials)")
435
+ case strings.Contains(err.Error(), "401"):
436
+ return errors.New("authentication failed (HTTP 401; check the remote's bearerTokenEnv)")
437
+ }
438
+ return err
439
+ }
440
+
441
+ func (r *Remotes) list(ctx context.Context, name string) ([]TaskSummary, error) {
442
+ ra, err := r.remote(name)
443
+ if err != nil {
444
+ return nil, err
445
+ }
446
+ ctx, cancel := r.call(ctx, ra)
447
+ defer cancel()
448
+ c, err := r.client(ctx, name, ra)
449
+ if err != nil {
450
+ return nil, err
451
+ }
452
+ res, err := c.ListTasks(ctx, &a2a.ListTasksRequest{IncludeArtifacts: true})
453
+ if err != nil {
454
+ return nil, fmt.Errorf("a2a: %q: %w", name, describe(err))
455
+ }
456
+ out := make([]TaskSummary, 0, len(res.Tasks))
457
+ for _, t := range res.Tasks {
458
+ out = append(out, summarize(t))
459
+ }
460
+ return out, nil
461
+ }
462
+
463
+ // GetTask fetches a remote task.
464
+ func (r *Remotes) GetTask(ctx context.Context, name, taskID string) (TaskSummary, error) {
465
+ ra, err := r.remote(name)
466
+ if err != nil {
467
+ return TaskSummary{}, err
468
+ }
469
+ ctx, cancel := r.call(ctx, ra)
470
+ defer cancel()
471
+ c, err := r.client(ctx, name, ra)
472
+ if err != nil {
473
+ return TaskSummary{}, err
474
+ }
475
+ t, err := c.GetTask(ctx, &a2a.GetTaskRequest{ID: a2a.TaskID(taskID)})
476
+ if err != nil {
477
+ return TaskSummary{}, fmt.Errorf("a2a: %q: %w", name, describe(err))
478
+ }
479
+ return summarize(t), nil
480
+ }
481
+
482
+ // CancelTask cancels a remote task.
483
+ func (r *Remotes) CancelTask(ctx context.Context, name, taskID string) (TaskSummary, error) {
484
+ ra, err := r.remote(name)
485
+ if err != nil {
486
+ return TaskSummary{}, err
487
+ }
488
+ ctx, cancel := r.call(ctx, ra)
489
+ defer cancel()
490
+ c, err := r.client(ctx, name, ra)
491
+ if err != nil {
492
+ return TaskSummary{}, err
493
+ }
494
+ t, err := c.CancelTask(ctx, &a2a.CancelTaskRequest{ID: a2a.TaskID(taskID)})
495
+ if err != nil {
496
+ return TaskSummary{}, fmt.Errorf("a2a: %q: %w", name, describe(err))
497
+ }
498
+ return summarize(t), nil
499
+ }