返回 DeepSeek-Reasonix
sdkgo.go
1 package protocolgen
2
3 import (
4 "encoding/json"
5 "fmt"
6 "go/format"
7 "reflect"
8 "strconv"
9 "strings"
10
11 "reasonix/internal/extension/protocol"
12 )
13
14 // The SDK types artifact (SDKTypesArtifactPath in generate.go) is produced
15 // from the same frozen registry and reflection walk as the JSON Schema:
16 // every wire DTO and enum reachable from the registry, plus the payload
17 // documents addressed indirectly through json.RawMessage fields and the
18 // structured error envelope.
19
20 // sdkEnumPrefixes maps every reachable string enum type to the constant-name
21 // prefix its values carry in the SDK's public API. ProviderErrorCode has
22 // none: its values already read as Go names ("provider_failed" →
23 // ProviderFailed).
24 var sdkEnumPrefixes = map[string]string{
25 "InterceptEvent": "Event",
26 "InterceptDecision": "Decision",
27 "UIHostKind": "UIHost",
28 "UISurfaceKind": "UISurface",
29 "UIRequestKind": "UIRequest",
30 "UIFieldKind": "UIField",
31 "UISeverity": "UISeverity",
32 "ProviderRole": "ProviderRole",
33 "ProviderChunkType": "Chunk",
34 "ProviderErrorCode": "",
35 "ContentEncoding": "Content",
36 "ErrorReason": "Err",
37 }
38
39 // sdkEnumConstantExceptions pins constant names that mechanical mangling
40 // would render differently from the SDK's established public API.
41 var sdkEnumConstantExceptions = map[string]map[string]string{
42 "ProviderChunkType": {"tool_call_args_delta": "ChunkToolCallDelta"},
43 }
44
45 // sdkInitialisms upper-cases identifier parts that read as acronyms.
46 var sdkInitialisms = map[string]string{
47 "ui": "UI", "tui": "TUI", "acp": "ACP", "utf8": "UTF8",
48 }
49
50 var rawMessageType = reflect.TypeOf(json.RawMessage{})
51
52 // sdkTypeWalk is the deterministic first-visit record of every named wire
53 // type reachable from the frozen registry (plus the extra roots below).
54 type sdkTypeWalk struct {
55 enums map[string][]string // frozen enum value sets, by type name
56 order []reflect.Type // named types in discovery order
57 kinds map[reflect.Type]string
58 seen map[reflect.Type]bool
59 }
60
61 // walkSDKTypes reflection-walks the frozen registry exactly like the schema
62 // builder does — registry params and non-notification results — and adds the
63 // roots no registry DTO references by name: the host UI payload documents
64 // (carried inside json.RawMessage payload fields) and the structured error
65 // envelope ProtocolErrorData.
66 func walkSDKTypes() (*sdkTypeWalk, error) {
67 w := &sdkTypeWalk{
68 enums: protocol.EnumValues(),
69 kinds: map[reflect.Type]string{},
70 seen: map[reflect.Type]bool{},
71 }
72 var roots []reflect.Type
73 for _, spec := range protocol.Registry() {
74 roots = append(roots, spec.ParamsType)
75 if !spec.Notification() {
76 roots = append(roots, spec.ResultType)
77 }
78 }
79 roots = append(roots,
80 reflect.TypeOf(protocol.UIStatusPayload{}),
81 reflect.TypeOf(protocol.UICardPayload{}),
82 reflect.TypeOf(protocol.UIFormPayload{}),
83 reflect.TypeOf(protocol.UINotificationPayload{}),
84 reflect.TypeOf(protocol.ProtocolErrorData{}),
85 )
86 for _, root := range roots {
87 if err := w.visit(root); err != nil {
88 return nil, err
89 }
90 }
91 return w, nil
92 }
93
94 func (w *sdkTypeWalk) visit(typ reflect.Type) error {
95 for typ.Kind() == reflect.Pointer {
96 typ = typ.Elem()
97 }
98 if typ == rawMessageType {
99 return nil
100 }
101 switch typ.Kind() {
102 case reflect.Struct:
103 if typ.Name() == "" {
104 return fmt.Errorf("anonymous struct %v is not a named wire DTO", typ)
105 }
106 if w.seen[typ] {
107 return nil
108 }
109 w.seen[typ] = true
110 w.order = append(w.order, typ)
111 w.kinds[typ] = "struct"
112 for i := 0; i < typ.NumField(); i++ {
113 field := typ.Field(i)
114 if field.PkgPath != "" {
115 continue
116 }
117 if field.Anonymous {
118 return fmt.Errorf("embedded field %v is not supported in wire DTOs", field.Type)
119 }
120 if err := w.visit(field.Type); err != nil {
121 return err
122 }
123 }
124 return nil
125 case reflect.String:
126 if typ.PkgPath() == "" {
127 return nil // predeclared string
128 }
129 if _, ok := w.enums[typ.Name()]; !ok {
130 return fmt.Errorf("named string type %s (%v) is not a frozen enum", typ.Name(), typ)
131 }
132 if w.seen[typ] {
133 return nil
134 }
135 w.seen[typ] = true
136 w.order = append(w.order, typ)
137 w.kinds[typ] = "enum"
138 return nil
139 case reflect.Slice, reflect.Array:
140 return w.visit(typ.Elem())
141 case reflect.Map:
142 if err := w.visit(typ.Key()); err != nil {
143 return err
144 }
145 return w.visit(typ.Elem())
146 }
147 // Predeclared scalars and unconstrained interfaces carry no named types.
148 return nil
149 }
150
151 // generateSDKTypesGo renders the SDK's DTO mirror from the frozen walk.
152 func generateSDKTypesGo() ([]byte, error) {
153 walk, err := walkSDKTypes()
154 if err != nil {
155 return nil, err
156 }
157 var out strings.Builder
158 out.WriteString("// Code generated by cmd/extension-protocol-gen; DO NOT EDIT.\n")
159 out.WriteString("\n")
160 out.WriteString("// Package extension: Extension Protocol v1 wire DTOs, enums, method\n")
161 out.WriteString("// names, frozen limits, and the frozen error table, mirrored from the\n")
162 out.WriteString("// host's internal/extension/protocol package. Behavior (validators,\n")
163 out.WriteString("// error constructors, helpers) lives in the handwritten files.\n")
164 out.WriteString("package extension\n")
165 out.WriteString("\n")
166 out.WriteString("import \"encoding/json\"\n\n")
167
168 emitSDKIdentity(&out)
169 emitSDKLimits(&out)
170 if err := emitSDKMethods(&out); err != nil {
171 return nil, err
172 }
173 emitSDKErrorTable(&out)
174 for _, typ := range walk.order {
175 var err error
176 switch walk.kinds[typ] {
177 case "enum":
178 err = emitSDKEnum(&out, walk, typ)
179 case "struct":
180 err = emitSDKStruct(&out, typ)
181 }
182 if err != nil {
183 return nil, err
184 }
185 }
186 if err := guardSDKSurfacePayloads(walk); err != nil {
187 return nil, err
188 }
189 formatted, err := format.Source([]byte(out.String()))
190 if err != nil {
191 return nil, fmt.Errorf("format sdk types source: %w", err)
192 }
193 return formatted, nil
194 }
195
196 func emitSDKIdentity(out *strings.Builder) {
197 out.WriteString("// ProtocolID is the immutable identity string peers exchange during the\n")
198 out.WriteString("// initialize handshake.\n")
199 fmt.Fprintf(out, "const ProtocolID = %q\n\n", protocol.ProtocolID)
200 out.WriteString("// ProtocolMajor is the frozen major version of this protocol build.\n")
201 fmt.Fprintf(out, "const ProtocolMajor = %d\n\n", protocol.ProtocolMajor)
202 out.WriteString("// ProtocolVersion is the wire string form of ProtocolMajor carried in the\n")
203 out.WriteString("// initialize handshake.\n")
204 fmt.Fprintf(out, "const ProtocolVersion = %q\n\n", protocol.ProtocolVersion)
205 }
206
207 func emitSDKLimits(out *strings.Builder) {
208 limits := protocol.FrozenLimits()
209 out.WriteString("// Frozen wire limits. These constants are part of the protocol contract.\n")
210 out.WriteString("const (\n")
211 out.WriteString("\t// FrameBytes caps one JSON-RPC frame on the extension transport.\n")
212 fmt.Fprintf(out, "\tFrameBytes = %d\n", limits.FrameBytes)
213 out.WriteString("\t// ExternalizeFieldBytes is the threshold above which an externalizable\n")
214 out.WriteString("\t// payload must move into a content ref instead of traveling inline.\n")
215 fmt.Fprintf(out, "\tExternalizeFieldBytes = %d\n", limits.ExternalizeFieldBytes)
216 out.WriteString("\t// ContentRefChunkBytes caps one host/content/read chunk.\n")
217 fmt.Fprintf(out, "\tContentRefChunkBytes = %d\n", limits.ContentRefChunkBytes)
218 out.WriteString("\t// ContentRefObjectBytes caps one externalized object.\n")
219 fmt.Fprintf(out, "\tContentRefObjectBytes = %d\n", limits.ContentRefObjectBytes)
220 out.WriteString(")\n\n")
221 }
222
223 func emitSDKMethods(out *strings.Builder) error {
224 out.WriteString("// Method names, frozen for Extension Protocol v1.\n")
225 out.WriteString("const (\n")
226 seen := map[string]bool{}
227 for _, spec := range protocol.Registry() {
228 name := sdkMethodConstantName(string(spec.Name))
229 if seen[name] {
230 return fmt.Errorf("method constant name collision: %s", name)
231 }
232 seen[name] = true
233 fmt.Fprintf(out, "\t%s = %q\n", name, string(spec.Name))
234 }
235 out.WriteString(")\n\n")
236 return nil
237 }
238
239 func emitSDKErrorTable(out *strings.Builder) {
240 out.WriteString("// DomainErrorCode is the JSON-RPC code every extension domain error uses on\n")
241 out.WriteString("// the wire. The structured ProtocolErrorData reason distinguishes them.\n")
242 fmt.Fprintf(out, "const DomainErrorCode = %d\n\n", protocol.DomainErrorCode)
243 out.WriteString("// errorSpec is one frozen error table entry: the JSON-RPC code, the\n")
244 out.WriteString("// generic wire message, and whether the call may be retried.\n")
245 out.WriteString("type errorSpec struct {\n\tCode int\n\tMessage string\n\tRetryable bool\n}\n\n")
246 out.WriteString("// frozenErrorSpecs mirrors the host's frozen error table. Adding an entry\n")
247 out.WriteString("// is a conscious protocol change.\n")
248 out.WriteString("var frozenErrorSpecs = map[ErrorReason]errorSpec{\n")
249 for _, contract := range protocol.ErrorContracts() {
250 fmt.Fprintf(out, "\t%s: {%s, %q, %t},\n",
251 sdkEnumConstantName("ErrorReason", string(contract.Reason)),
252 sdkErrorCodeName(contract.JSONRPCCode), contract.Message, contract.Retryable)
253 }
254 out.WriteString("}\n\n")
255 }
256
257 // sdkErrorCodeName renders a frozen JSON-RPC code with the SDK's symbolic
258 // constant where one exists (the standard codes live in wire.go).
259 func sdkErrorCodeName(code int) string {
260 switch code {
261 case -32600:
262 return "CodeInvalidRequest"
263 case -32601:
264 return "CodeMethodNotFound"
265 case -32602:
266 return "CodeInvalidParams"
267 case -32603:
268 return "CodeInternal"
269 case protocol.DomainErrorCode:
270 return "DomainErrorCode"
271 default:
272 return strconv.Itoa(code)
273 }
274 }
275
276 func emitSDKEnum(out *strings.Builder, walk *sdkTypeWalk, typ reflect.Type) error {
277 name := typ.Name()
278 if _, ok := sdkEnumPrefixes[name]; !ok {
279 return fmt.Errorf("enum %s has no constant prefix registered", name)
280 }
281 fmt.Fprintf(out, "// %s is a generated Extension Protocol v1 string enum.\n", name)
282 fmt.Fprintf(out, "type %s string\n\n", name)
283 out.WriteString("const (\n")
284 seen := map[string]bool{}
285 for _, value := range walk.enums[name] {
286 constant := sdkEnumConstantName(name, value)
287 if seen[constant] {
288 return fmt.Errorf("enum constant name collision: %s", constant)
289 }
290 seen[constant] = true
291 fmt.Fprintf(out, "\t%s %s = %q\n", constant, name, value)
292 }
293 out.WriteString(")\n\n")
294 return nil
295 }
296
297 func emitSDKStruct(out *strings.Builder, typ reflect.Type) error {
298 fmt.Fprintf(out, "// %s is a generated Extension Protocol v1 wire DTO.\n", typ.Name())
299 fields := 0
300 var body strings.Builder
301 for i := 0; i < typ.NumField(); i++ {
302 field := typ.Field(i)
303 if field.PkgPath != "" {
304 continue
305 }
306 rendered, err := renderSDKType(field.Type)
307 if err != nil {
308 return fmt.Errorf("%s field %s: %w", typ.Name(), field.Name, err)
309 }
310 fmt.Fprintf(&body, "\t%s %s `%s`\n", field.Name, rendered, string(field.Tag))
311 fields++
312 }
313 if fields == 0 {
314 fmt.Fprintf(out, "type %s struct{}\n\n", typ.Name())
315 return nil
316 }
317 fmt.Fprintf(out, "type %s struct {\n%s}\n\n", typ.Name(), body.String())
318 return nil
319 }
320
321 // renderSDKType renders a field type as Go source. Named types resolve to
322 // their bare name — every named type reachable from the walk is either one of
323 // the mirrored DTOs/enums or encoding/json's RawMessage.
324 func renderSDKType(typ reflect.Type) (string, error) {
325 if typ == rawMessageType {
326 return "json.RawMessage", nil
327 }
328 switch typ.Kind() {
329 case reflect.Pointer:
330 elem, err := renderSDKType(typ.Elem())
331 return "*" + elem, err
332 case reflect.Slice:
333 elem, err := renderSDKType(typ.Elem())
334 return "[]" + elem, err
335 case reflect.Map:
336 key, err := renderSDKType(typ.Key())
337 if err != nil {
338 return "", err
339 }
340 elem, err := renderSDKType(typ.Elem())
341 return "map[" + key + "]" + elem, err
342 case reflect.Interface:
343 if typ.NumMethod() == 0 {
344 return "any", nil
345 }
346 case reflect.Struct:
347 if typ.Name() != "" {
348 return typ.Name(), nil
349 }
350 case reflect.String:
351 if typ.Name() != "" {
352 return typ.Name(), nil
353 }
354 return "string", nil
355 case reflect.Bool:
356 return "bool", nil
357 case reflect.Int:
358 return "int", nil
359 case reflect.Int64:
360 return "int64", nil
361 case reflect.Uint64:
362 return "uint64", nil
363 case reflect.Float64:
364 return "float64", nil
365 }
366 return "", fmt.Errorf("unsupported wire type %v", typ)
367 }
368
369 // guardSDKSurfacePayloads pins the surface-kind → payload-DTO convention: a
370 // new UISurfaceKind without a matching UI<Kind>Payload type in the generated
371 // set fails generation loudly instead of silently shipping an SDK that
372 // cannot build the new surface.
373 func guardSDKSurfacePayloads(walk *sdkTypeWalk) error {
374 emitted := map[string]bool{}
375 for _, typ := range walk.order {
376 emitted[typ.Name()] = true
377 }
378 for _, kind := range walk.enums["UISurfaceKind"] {
379 want := "UI" + sdkIdentifierFromValue(kind) + "Payload"
380 if !emitted[want] {
381 return fmt.Errorf("surface kind %q has no payload DTO %s in the generated set", kind, want)
382 }
383 }
384 return nil
385 }
386
387 // sdkMethodConstantName mangles a wire method name into its Go constant
388 // name: "extension/ui/action" → MethodExtensionUIAction.
389 func sdkMethodConstantName(method string) string {
390 return "Method" + sdkIdentifierFromValue(method)
391 }
392
393 // sdkEnumConstantName mangles an enum value into its Go constant name with
394 // the type's registered prefix; exceptions pin the established public API.
395 func sdkEnumConstantName(typeName, value string) string {
396 if exceptions, ok := sdkEnumConstantExceptions[typeName]; ok {
397 if name, ok := exceptions[value]; ok {
398 return name
399 }
400 }
401 return sdkEnumPrefixes[typeName] + sdkIdentifierFromValue(value)
402 }
403
404 // sdkIdentifierFromValue turns a wire value into Go identifier parts:
405 // "agent.before_start" → "AgentBeforeStart".
406 func sdkIdentifierFromValue(value string) string {
407 parts := strings.FieldsFunc(value, func(r rune) bool {
408 return r == '_' || r == '.' || r == '-' || r == '/'
409 })
410 var out strings.Builder
411 for _, part := range parts {
412 if initialism, ok := sdkInitialisms[part]; ok {
413 out.WriteString(initialism)
414 continue
415 }
416 out.WriteString(strings.ToUpper(part[:1]) + part[1:])
417 }
418 return out.String()
419 }
420
420 lines GO