| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | |
| 6 | "reasonix/internal/attachment" |
| 7 | "reasonix/internal/provider" |
| 8 | ) |
| 9 | |
| 10 | type rawUserInputKey struct{} |
| 11 | type subagentImageCandidatesKey struct{} |
| 12 | type subagentImageInputsKey struct{} |
| 13 | type userImageInputsContextKey struct{} |
| 14 | type visionSummaryContextKey struct{} |
| 15 | |
| 16 | // WithRawUserInput keeps user-authored text separate from host-rendered turn |
| 17 | // context. Runner implementations can persist the raw text while sending their |
| 18 | // composed input to the provider. |
| 19 | func WithRawUserInput(ctx context.Context, raw string) context.Context { |
| 20 | if ctx == nil { |
| 21 | ctx = context.Background() |
| 22 | } |
| 23 | return context.WithValue(ctx, rawUserInputKey{}, raw) |
| 24 | } |
| 25 | |
| 26 | // RawUserInput returns the host-authenticated raw user text when available. |
| 27 | // Direct Agent callers that do not use a Controller keep their input unchanged. |
| 28 | func RawUserInput(ctx context.Context, fallback string) string { |
| 29 | if ctx == nil { |
| 30 | return fallback |
| 31 | } |
| 32 | if raw, ok := ctx.Value(rawUserInputKey{}).(string); ok { |
| 33 | return raw |
| 34 | } |
| 35 | return fallback |
| 36 | } |
| 37 | |
| 38 | // WithSubagentImageCandidates carries attachment data resolved by the parent |
| 39 | // controller for a child model to opt into when it supports vision. Keeping |
| 40 | // this separate from UserImages avoids changing the parent's provider-visible |
| 41 | // text-only turn while allowing a vision-capable child to receive the same |
| 42 | // authorized attachments. |
| 43 | func WithSubagentImageCandidates(ctx context.Context, images []string) context.Context { |
| 44 | if ctx == nil { |
| 45 | ctx = context.Background() |
| 46 | } |
| 47 | return context.WithValue(ctx, subagentImageCandidatesKey{}, append([]string(nil), images...)) |
| 48 | } |
| 49 | |
| 50 | // SubagentImageCandidates returns the parent-resolved attachment data that a |
| 51 | // child provider may use when its own model supports vision. |
| 52 | func SubagentImageCandidates(ctx context.Context) []string { |
| 53 | if ctx == nil { |
| 54 | return nil |
| 55 | } |
| 56 | images, _ := ctx.Value(subagentImageCandidatesKey{}).([]string) |
| 57 | return append([]string(nil), images...) |
| 58 | } |
| 59 | |
| 60 | // WithSubagentImageInputs carries durable attachment refs from the parent |
| 61 | // turn. A vision-capable child resolves them through the inherited request |
| 62 | // resolver instead of inheriting already-wired data URLs. |
| 63 | func WithSubagentImageInputs(ctx context.Context, inputs []attachment.ImageInput) context.Context { |
| 64 | if ctx == nil { |
| 65 | ctx = context.Background() |
| 66 | } |
| 67 | return context.WithValue(ctx, subagentImageInputsKey{}, attachment.CloneImageInputs(inputs)) |
| 68 | } |
| 69 | |
| 70 | // SubagentImageInputs returns the parent-admitted image refs a child may |
| 71 | // persist on its user message. Empty when the parent only had legacy Images. |
| 72 | func SubagentImageInputs(ctx context.Context) []attachment.ImageInput { |
| 73 | if ctx == nil { |
| 74 | return nil |
| 75 | } |
| 76 | inputs, _ := ctx.Value(subagentImageInputsKey{}).([]attachment.ImageInput) |
| 77 | return attachment.CloneImageInputs(inputs) |
| 78 | } |
| 79 | |
| 80 | func WithUserImageInputs(ctx context.Context, inputs []attachment.ImageInput) context.Context { |
| 81 | if ctx == nil { |
| 82 | ctx = context.Background() |
| 83 | } |
| 84 | return context.WithValue(ctx, userImageInputsContextKey{}, attachment.CloneImageInputs(inputs)) |
| 85 | } |
| 86 | |
| 87 | func userImageInputs(ctx context.Context) []attachment.ImageInput { |
| 88 | inputs, _ := ctx.Value(userImageInputsContextKey{}).([]attachment.ImageInput) |
| 89 | return attachment.CloneImageInputs(inputs) |
| 90 | } |
| 91 | |
| 92 | func withSubagentTurnImages(ctx context.Context) context.Context { |
| 93 | if inputs := SubagentImageInputs(ctx); len(inputs) > 0 { |
| 94 | return WithUserImageInputs(ctx, inputs) |
| 95 | } |
| 96 | return WithUserImages(ctx, SubagentImageCandidates(ctx)) |
| 97 | } |
| 98 | |
| 99 | // WithVisionSummary carries a hidden image-understanding result into the |
| 100 | // foreground turn. The run loop persists it on the user message while keeping |
| 101 | // RawContent as the user-visible text. |
| 102 | func WithVisionSummary(ctx context.Context, summary *provider.VisionSummary) context.Context { |
| 103 | if ctx == nil { |
| 104 | ctx = context.Background() |
| 105 | } |
| 106 | if summary == nil { |
| 107 | return ctx |
| 108 | } |
| 109 | cp := *summary |
| 110 | cp.ImageDigests = append([]string(nil), summary.ImageDigests...) |
| 111 | return context.WithValue(ctx, visionSummaryContextKey{}, &cp) |
| 112 | } |
| 113 | |
| 114 | func VisionSummaryFromContext(ctx context.Context) *provider.VisionSummary { |
| 115 | if ctx == nil { |
| 116 | return nil |
| 117 | } |
| 118 | summary, _ := ctx.Value(visionSummaryContextKey{}).(*provider.VisionSummary) |
| 119 | if summary == nil { |
| 120 | return nil |
| 121 | } |
| 122 | cp := *summary |
| 123 | cp.ImageDigests = append([]string(nil), summary.ImageDigests...) |
| 124 | return &cp |
| 125 | } |
| 126 |