| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | |
| 7 | "reasonix/internal/attachment" |
| 8 | "reasonix/internal/imageinput" |
| 9 | "reasonix/internal/provider" |
| 10 | "reasonix/internal/tool" |
| 11 | ) |
| 12 | |
| 13 | type agentImageInput struct { |
| 14 | service *imageinput.Service |
| 15 | native bool |
| 16 | } |
| 17 | type imageResult struct { |
| 18 | text string |
| 19 | summary *provider.VisionSummary |
| 20 | diagnostic error |
| 21 | } |
| 22 | |
| 23 | func newImageInput(cfg *imageinput.Config, p provider.Provider) agentImageInput { |
| 24 | if cfg == nil { |
| 25 | return agentImageInput{native: supportsNativeImages(p)} |
| 26 | } |
| 27 | return agentImageInput{service: imageinput.New(*cfg), native: supportsNativeImages(p)} |
| 28 | } |
| 29 | func (a *Agent) ImageInput() *imageinput.Service { return a.imageInput.service } |
| 30 | |
| 31 | // ImageRequestResolver turns durable ImageInputs into provider-visible Images |
| 32 | // on a request copy. It must not write variants or uploads back to history. |
| 33 | type ImageRequestResolver interface { |
| 34 | ResolveRequestImages(ctx context.Context, msgs []provider.Message) ([]provider.Message, error) |
| 35 | PersistToolImages(ctx context.Context, images []string) ([]attachment.ImageInput, error) |
| 36 | } |
| 37 | |
| 38 | func (a *Agent) SetImageRequestResolver(resolver ImageRequestResolver) { |
| 39 | if a == nil { |
| 40 | return |
| 41 | } |
| 42 | a.imageResolver = resolver |
| 43 | if a.svc.tools == nil { |
| 44 | return |
| 45 | } |
| 46 | if item, ok := a.svc.tools.Get(tool.HostTask); ok { |
| 47 | if task, ok := item.(*TaskTool); ok { |
| 48 | task.imageResolver = resolver |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | func (a *Agent) processToolImages(ctx context.Context, text string, images []string) imageResult { |
| 53 | if len(images) == 0 { |
| 54 | return imageResult{text: text} |
| 55 | } |
| 56 | // Durable images are persisted by buildBatchToolResult first. Request |
| 57 | // assembly then selects the actual native or understanding model route. |
| 58 | if a.imageInput.native || a.imageResolver != nil { |
| 59 | return imageResult{text: text} |
| 60 | } |
| 61 | summary, err := a.imageInput.service.Understand(ctx, a.modelRef, images, a.Session().Snapshot, a.svc.sink) |
| 62 | if err != nil { |
| 63 | return imageResult{text: text + fmt.Sprintf("\n[Image understanding unavailable: %v. The tool already executed; its text result remains valid. Do not claim to have seen the image or repeat the original action to retry image understanding.]", err), diagnostic: err} |
| 64 | } |
| 65 | return imageResult{text: imageinput.AppendSummary(text, summary), summary: summary} |
| 66 | } |
| 67 | |
| 68 | func supportsNativeImages(p provider.Provider) bool { |
| 69 | info, ok := p.(provider.ModelInfoProvider) |
| 70 | return ok && info.ModelInfo().SupportsInput(provider.ModalityImage) |
| 71 | } |
| 72 |