返回 DeepSeek-Reasonix
registry.go
根目录 / internal / extension / protocol / registry.go
1 package protocol
2
3 import (
4 "encoding/json"
5 "fmt"
6 "reflect"
7 "sort"
8 )
9
10 type Method string
11
12 const (
13 // Lifecycle (Host → Extension).
14 MethodExtensionInitialize Method = "extension/initialize"
15 MethodExtensionInitialized Method = "extension/initialized"
16 MethodExtensionShutdown Method = "extension/shutdown"
17 MethodExtensionIntercept Method = "extension/intercept"
18 MethodExtensionEvent Method = "extension/event"
19 MethodExtensionResourcesChanged Method = "extension/resources/changed"
20
21 // Extension-hosted provider broker. Catalog/open/cancel run Host →
22 // Extension; stream chunks and stream end flow back as notifications.
23 MethodExtensionProviderCatalog Method = "extension/provider/catalog"
24 MethodExtensionProviderStreamOpen Method = "extension/provider/stream/open"
25 MethodExtensionProviderStreamCancel Method = "extension/provider/stream/cancel"
26 MethodExtensionProviderStreamChunk Method = "extension/provider/stream/chunk"
27 MethodExtensionProviderStreamEnd Method = "extension/provider/stream/end"
28
29 // UI. Action invocations and form submissions run Host → Extension;
30 // surfaces and blocking prompts are Extension → Host requests.
31 MethodExtensionUIAction Method = "extension/ui/action"
32 MethodExtensionUISubmit Method = "extension/ui/submit"
33 MethodHostUIPublish Method = "host/ui/publish"
34 MethodHostUIRequest Method = "host/ui/request"
35
36 // Externalized content reads (Extension → Host).
37 MethodHostContentRead Method = "host/content/read"
38 )
39
40 // MethodSpec is one frozen registry entry: the method name, its direction,
41 // its operation class, and the exact wire DTOs for params and result.
42 type MethodSpec struct {
43 Name Method
44 Direction Direction
45 Class OperationClass
46 ParamsType reflect.Type
47 ResultType reflect.Type
48 }
49
50 // Notification reports whether the method carries no response.
51 func (s MethodSpec) Notification() bool { return s.Direction.IsNotification() }
52
53 func hostRequest[P, R any](name Method, class OperationClass) MethodSpec {
54 return MethodSpec{name, DirectionHostToExtensionRequest, class, typeOf[P](), typeOf[R]()}
55 }
56
57 func extensionRequest[P, R any](name Method, class OperationClass) MethodSpec {
58 return MethodSpec{name, DirectionExtensionToHostRequest, class, typeOf[P](), typeOf[R]()}
59 }
60
61 func hostNotification[P any](name Method, class OperationClass) MethodSpec {
62 return MethodSpec{name, DirectionHostToExtensionNotification, class, typeOf[P](), typeOf[NoResult]()}
63 }
64
65 func extensionNotification[P any](name Method, class OperationClass) MethodSpec {
66 return MethodSpec{name, DirectionExtensionToHostNotification, class, typeOf[P](), typeOf[NoResult]()}
67 }
68
69 func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() }
70
71 // frozenRegistry is the Extension Protocol v1 method set. Adding, renaming,
72 // or redirecting a method is a conscious protocol change: ValidateRegistry
73 // pins the counts and the generated schema hash changes.
74 var frozenRegistry = []MethodSpec{
75 hostRequest[InitializeParams, InitializeResult](MethodExtensionInitialize, ClassLifecycle),
76 hostNotification[InitializedParams](MethodExtensionInitialized, ClassLifecycle),
77 hostRequest[ShutdownParams, ShutdownResult](MethodExtensionShutdown, ClassLifecycle),
78 hostRequest[InterceptParams, InterceptResult](MethodExtensionIntercept, ClassIntercept),
79 hostNotification[EventParams](MethodExtensionEvent, ClassObservation),
80 hostNotification[ResourcesChangedParams](MethodExtensionResourcesChanged, ClassObservation),
81 hostRequest[ProviderCatalogParams, ProviderCatalogResult](MethodExtensionProviderCatalog, ClassProvider),
82 hostRequest[StreamOpenParams, StreamOpenResult](MethodExtensionProviderStreamOpen, ClassProvider),
83 hostRequest[StreamCancelParams, StreamCancelResult](MethodExtensionProviderStreamCancel, ClassProvider),
84 extensionNotification[StreamChunkParams](MethodExtensionProviderStreamChunk, ClassProvider),
85 extensionNotification[StreamEndParams](MethodExtensionProviderStreamEnd, ClassProvider),
86 hostRequest[UIActionParams, UIActionResult](MethodExtensionUIAction, ClassUI),
87 hostRequest[UISubmitParams, UISubmitResult](MethodExtensionUISubmit, ClassUI),
88 extensionRequest[UIPublishParams, UIPublishResult](MethodHostUIPublish, ClassUI),
89 extensionRequest[UIRequestParams, UIRequestResult](MethodHostUIRequest, ClassUI),
90 extensionRequest[ContentReadParams, ContentReadResult](MethodHostContentRead, ClassContent),
91 }
92
93 var frozenRegistryByName = buildRegistryIndex(frozenRegistry)
94
95 func buildRegistryIndex(specs []MethodSpec) map[Method]MethodSpec {
96 index := make(map[Method]MethodSpec, len(specs))
97 for _, spec := range specs {
98 if _, duplicate := index[spec.Name]; duplicate {
99 panic("protocol: duplicate method " + string(spec.Name))
100 }
101 if spec.ParamsType.Kind() != reflect.Struct || spec.ResultType.Kind() != reflect.Struct {
102 panic("protocol: method types must be structs: " + string(spec.Name))
103 }
104 index[spec.Name] = spec
105 }
106 return index
107 }
108
109 // Registry returns the frozen methods sorted by name.
110 func Registry() []MethodSpec {
111 out := append([]MethodSpec(nil), frozenRegistry...)
112 sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
113 return out
114 }
115
116 // LookupMethod finds one frozen method by name.
117 func LookupMethod(name Method) (MethodSpec, bool) {
118 spec, ok := frozenRegistryByName[name]
119 return spec, ok
120 }
121
122 // DecodeHostRequestParams applies the registry's exact DTO and strict decoder
123 // for a Host → Extension request.
124 func DecodeHostRequestParams(method Method, raw json.RawMessage) (any, error) {
125 return decodeForDirection(method, raw, DirectionHostToExtensionRequest, true)
126 }
127
128 // DecodeHostRequestResult applies the frozen result DTO for a Host →
129 // Extension request; the extension side decodes host answers with it.
130 func DecodeHostRequestResult(method Method, raw json.RawMessage) (any, error) {
131 return decodeForDirection(method, raw, DirectionHostToExtensionRequest, false)
132 }
133
134 // DecodeExtensionRequestParams applies the registry's exact DTO and strict
135 // decoder for an Extension → Host request.
136 func DecodeExtensionRequestParams(method Method, raw json.RawMessage) (any, error) {
137 return decodeForDirection(method, raw, DirectionExtensionToHostRequest, true)
138 }
139
140 // DecodeExtensionRequestResult applies the frozen result DTO for an
141 // Extension → Host request; the host side decodes extension answers with it.
142 func DecodeExtensionRequestResult(method Method, raw json.RawMessage) (any, error) {
143 return decodeForDirection(method, raw, DirectionExtensionToHostRequest, false)
144 }
145
146 // DecodeHostNotificationParams applies the strict decoder to a Host →
147 // Extension notification payload.
148 func DecodeHostNotificationParams(method Method, raw json.RawMessage) (any, error) {
149 return decodeForDirection(method, raw, DirectionHostToExtensionNotification, true)
150 }
151
152 // DecodeExtensionNotificationParams applies the strict decoder to an
153 // Extension → Host notification payload.
154 func DecodeExtensionNotificationParams(method Method, raw json.RawMessage) (any, error) {
155 return decodeForDirection(method, raw, DirectionExtensionToHostNotification, true)
156 }
157
158 func decodeForDirection(method Method, raw json.RawMessage, direction Direction, params bool) (any, error) {
159 spec, ok := LookupMethod(method)
160 if !ok {
161 return nil, fmt.Errorf("protocol: unregistered method %q", method)
162 }
163 if spec.Direction != direction {
164 return nil, fmt.Errorf("protocol: %q is not a %s", method, direction)
165 }
166 if params {
167 return decodeAndValidate(raw, spec.ParamsType)
168 }
169 if spec.Notification() {
170 return nil, fmt.Errorf("protocol: %q is a notification and has no result", method)
171 }
172 return decodeAndValidate(raw, spec.ResultType)
173 }
174
175 // ValidateRegistry pins the frozen method counts so adding a method is a
176 // conscious act that must update this contract and regenerate the schema.
177 func ValidateRegistry() error {
178 hostReq, extReq, hostNotif, extNotif := 0, 0, 0, 0
179 for _, spec := range frozenRegistry {
180 switch spec.Direction {
181 case DirectionHostToExtensionRequest:
182 hostReq++
183 case DirectionExtensionToHostRequest:
184 extReq++
185 case DirectionHostToExtensionNotification:
186 hostNotif++
187 case DirectionExtensionToHostNotification:
188 extNotif++
189 default:
190 return fmt.Errorf("method %s has invalid direction %q", spec.Name, spec.Direction)
191 }
192 }
193 // Extension Protocol v1: 8 lifecycle/intercept/provider/UI Host →
194 // Extension requests, 3 Extension → Host requests (UI publish/request,
195 // content read), 3 Host → Extension notifications, 2 provider stream
196 // notifications.
197 if len(frozenRegistry) != 16 || hostReq != 8 || extReq != 3 || hostNotif != 3 || extNotif != 2 {
198 return fmt.Errorf("registry count = total=%d hostReq=%d extReq=%d hostNotif=%d extNotif=%d, want 16/8/3/3/2",
199 len(frozenRegistry), hostReq, extReq, hostNotif, extNotif)
200 }
201 return nil
202 }
203
203 lines GO