1package extensions
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "time"
9
10 "github.com/patriceckhart/zot/packages/core"
11 "github.com/patriceckhart/zot/packages/provider"
12)
13
14// extensionTool wraps a single extension-registered tool as a
15// core.Tool. The agent's tool registry contains one of these per
16// extension tool; Execute round-trips through the manager to the
17// owning subprocess.
18//
19// One concrete type instead of a closure-driven anonymous tool
20// keeps the schema, name, and ownership inspectable for logs and
21// dialogs.
22type extensionTool struct {
23 name string
24 description string
25 schema json.RawMessage
26 extension string
27 manager *Manager
28 timeout time.Duration
29 deferred bool
30 interactive bool
31}
32
33// NewTool returns a core.Tool that round-trips invocations through
34// mgr to the extension that registered (name, schema). The default
35// per-call timeout is 60 seconds. Interactive tools have no reply deadline.
36func NewTool(mgr *Manager, info ToolInfo) core.Tool {
37 timeout := 60 * time.Second
38 if info.Interactive {
39 timeout = 0
40 }
41 return &extensionTool{
42 name: info.Name,
43 description: info.Description,
44 schema: info.Schema,
45 extension: info.Extension,
46 manager: mgr,
47 timeout: timeout,
48 deferred: info.Deferred,
49 interactive: info.Interactive,
50 }
51}
52
53func (t *extensionTool) Name() string { return t.name }
54func (t *extensionTool) Description() string { return t.description }
55func (t *extensionTool) Schema() json.RawMessage { return t.schema }
56func (t *extensionTool) Extension() string { return t.extension }
57func (t *extensionTool) Deferred() bool { return t.deferred }
58
59// Execute is what the agent calls when the LLM invokes the tool. It
60// hands args to the owning extension, waits up to t.timeout for the
61// reply, and converts the response into a core.ToolResult.
62func (t *extensionTool) Execute(ctx context.Context, args json.RawMessage, _ func(string)) (core.ToolResult, error) {
63 if len(args) == 0 {
64 args = json.RawMessage(`{}`)
65 }
66 if t.interactive && !t.manager.supportsInteractiveTools() {
67 return core.ToolResult{Status: "failed", IsError: true, Content: []provider.Content{
68 provider.TextBlock{Text: "interactive extension tools require an interactive host"},
69 }}, nil
70 }
71 resp, err := t.manager.InvokeTool(ctx, t.name, args, t.timeout)
72 if err != nil {
73 status := "failed"
74 if errors.Is(err, context.Canceled) {
75 status = "cancelled"
76 }
77 if errors.Is(err, context.DeadlineExceeded) {
78 status = "timed_out"
79 }
80 return core.ToolResult{
81 Status: status,
82 IsError: true,
83 Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("extension %s/%s failed: %v", t.extension, t.name, err)}},
84 }, nil
85 }
86 out := core.ToolResult{IsError: resp.IsError, ActivateTools: resp.ActivateTools}
87 for _, b := range resp.Content {
88 switch b.Type {
89 case "text":
90 if b.Text != "" {
91 out.Content = append(out.Content, provider.TextBlock{Text: b.Text})
92 }
93 case "image":
94 data, dErr := decodeBase64(b.Data)
95 if dErr != nil {
96 out.IsError = true
97 out.Content = append(out.Content, provider.TextBlock{Text: fmt.Sprintf("extension %s/%s returned undecodable image: %v", t.extension, t.name, dErr)})
98 continue
99 }
100 out.Content = append(out.Content, provider.ImageBlock{
101 MimeType: b.MimeType,
102 Data: data,
103 })
104 default:
105 // Unknown block type: stringify for debug visibility.
106 out.Content = append(out.Content, provider.TextBlock{Text: fmt.Sprintf("[unknown block type %q from extension %s]", b.Type, t.extension)})
107 }
108 }
109 if len(out.Content) == 0 {
110 // Defensive: an empty content slice would confuse the model.
111 out.Content = []provider.Content{provider.TextBlock{Text: "(extension returned no content)"}}
112 }
113 out.Details = map[string]any{"extension": t.extension, "tool": t.name}
114 return out, nil
115}
116
117// decodeBase64 is a tiny wrapper around encoding/base64 so we can
118// validate the extension's image data in one place.
119func decodeBase64(s string) ([]byte, error) {
120 if s == "" {
121 return nil, nil
122 }
123 return base64DecodeStd(s)
124}