From c0abfe5229f5c490f14bbc7d7ab5869b8da8630f Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Thu, 4 Jun 2026 18:18:48 -0700 Subject: [PATCH 1/2] cmd/a2uigen: add Go SDK generator --- agent_sdks/go/cmd/a2uigen/main.go | 1273 +++++++++++++++++ agent_sdks/go/cmd/a2uigen/main_test.go | 64 + .../go/cmd/a2uigen/templates/types.go.txtar | 358 +++++ agent_sdks/go/go.mod | 5 + agent_sdks/go/go.sum | 2 + 5 files changed, 1702 insertions(+) create mode 100644 agent_sdks/go/cmd/a2uigen/main.go create mode 100644 agent_sdks/go/cmd/a2uigen/main_test.go create mode 100644 agent_sdks/go/cmd/a2uigen/templates/types.go.txtar create mode 100644 agent_sdks/go/go.mod create mode 100644 agent_sdks/go/go.sum diff --git a/agent_sdks/go/cmd/a2uigen/main.go b/agent_sdks/go/cmd/a2uigen/main.go new file mode 100644 index 0000000000..6c17439815 --- /dev/null +++ b/agent_sdks/go/cmd/a2uigen/main.go @@ -0,0 +1,1273 @@ +// Command a2uigen generates Go source code for the a2ui and a2uibuild +// packages from the A2UI JSON Schema specification. +// +// Usage: +// +// go run ./cmd/a2uigen -schemas path/to/json -out . +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/format" + "io/fs" + "log" + "os" + "path" + "path/filepath" + "runtime" + "slices" + "sort" + "strings" + "text/template" + "unicode" + + _ "embed" + + "golang.org/x/tools/txtar" +) + +//go:embed templates/types.go.txtar +var templateData []byte + +func main() { + schemas := flag.String("schemas", "", "path to JSON schemas directory") + out := flag.String("out", "", "output directory") + pkg := flag.String("pkg", "a2ui", "Go package name for generated type files") + module := flag.String("module", "", "Go module path for generated imports (default: module containing -out)") + a2uiDir := flag.String("a2ui-dir", "a2ui", "directory for generated a2ui package, relative to -out") + buildDir := flag.String("build-dir", "a2uibuild", "directory for generated a2uibuild package, relative to -out") + oldBuildDir := flag.String("a2uibuild-dir", "", "alias for -build-dir") + a2uiImport := flag.String("a2ui-import", "", "import path for generated a2ui package (default: derived from -module and -a2ui-dir)") + sdk := flag.Bool("sdk", false, "generate the complete Go SDK, including static support files") + specRoot := flag.String("spec-root", "", "A2UI specification root for -sdk mode (default: inferred from generator checkout)") + sdkRoot := flag.String("sdk-root", "", "Go SDK source root for -sdk mode (default: inferred from generator)") + stable := flag.Bool("stable", false, "also generate a2ui/a2ui.go alias file") + flag.Parse() + if *out == "" || (!*sdk && *schemas == "") { + flag.Usage() + os.Exit(1) + } + if *oldBuildDir != "" { + *buildDir = *oldBuildDir + } + + if *sdk { + if err := generateSDK(*out, *module, *a2uiDir, *buildDir, *a2uiImport, *specRoot, *sdkRoot); err != nil { + log.Fatal(err) + } + return + } + + if err := generateFromSchemas(*schemas, *out, *pkg, *module, *a2uiDir, *buildDir, *a2uiImport, *stable); err != nil { + log.Fatal(err) + } +} + +func generateFromSchemas(schemas, out, pkg, module, a2uiDir, buildDir, a2uiImport string, stable bool) error { + catalogPath, err := findBasicCatalog(schemas) + if err != nil { + return err + } + catalog, err := parseCatalog(catalogPath) + if err != nil { + return err + } + commonTypes, err := parseCommonTypes(filepath.Join(schemas, "common_types.json")) + if err != nil { + return err + } + wrappers, err := parseWrappers(schemas) + if err != nil { + return err + } + outConfig, err := resolveOutputConfig(out, module, a2uiDir, buildDir, a2uiImport, pkg) + if err != nil { + return err + } + + data, err := buildTemplateData(catalog, commonTypes) + if err != nil { + return err + } + data.Wrappers = wrappers + data.Pkg = pkg + data.A2UIImport = outConfig.A2UIImport + data.VersionImport = outConfig.VersionImport + data.Stable = stable + + ar := txtar.Parse(templateData) + + funcMap := template.FuncMap{ + "pascalCase": pascalCase, + "goFieldName": goFieldName, + "qualifyBuilderType": qualifyBuilderType, + "sub": func(a, b int) int { return a - b }, + "add": func(a, b int) int { return a + b }, + "lower": strings.ToLower, + "join": strings.Join, + } + + for _, f := range ar.Files { + name := strings.TrimSpace(f.Name) + + // Stable facade files are only rendered when -stable is set. + if (name == "a2ui.go" || strings.Contains(name, "builders")) && !stable { + continue + } + + tmpl, err := template.New(name).Funcs(funcMap).Parse(string(f.Data)) + if err != nil { + return fmt.Errorf("parsing template %s: %w", name, err) + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return fmt.Errorf("executing template %s: %w", name, err) + } + formatted, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("formatting %s: %w\n%s", name, err, buf.String()) + } + + // Determine output directory. + outDir := filepath.Join(out, outConfig.A2UIDir) + if name == "a2ui.go" { + // Alias file always goes to the stable a2ui package. + outDir = filepath.Join(out, outConfig.A2UIDir) + } else if strings.Contains(name, "builders") { + outDir = filepath.Join(out, outConfig.A2UIBuildDir) + } else if pkg != "a2ui" { + // Type files go into {a2uiDir}/{pkg}/. + outDir = filepath.Join(out, outConfig.A2UIDir, pkg) + } + if err := os.MkdirAll(outDir, 0o755); err != nil { + return err + } + outPath := filepath.Join(outDir, name) + if err := os.WriteFile(outPath, formatted, 0o644); err != nil { + return err + } + fmt.Println(outPath) + } + return nil +} + +// Schema types for parsing the JSON schemas. + +type catalogFile struct { + Components map[string]json.RawMessage `json:"components"` + Functions map[string]json.RawMessage `json:"functions"` +} + +type componentSchema struct { + AllOf []json.RawMessage `json:"allOf"` +} + +type allOfItem struct { + Ref string `json:"$ref"` + Properties map[string]propertySchema `json:"properties"` + Required []string `json:"required"` +} + +type propertySchema struct { + Ref string `json:"$ref"` + Type string `json:"type"` + Enum []string `json:"enum"` + Const any `json:"const"` + Items *propertySchema `json:"items"` + OneOf []propertySchema `json:"oneOf"` + AllOf []propertySchema `json:"allOf"` + Description string `json:"description"` + Properties map[string]propertySchema `json:"properties"` +} + +type functionSchema struct { + Properties struct { + Call struct { + Const string `json:"const"` + } `json:"call"` + Args struct { + Properties map[string]propertySchema `json:"properties"` + Required []string `json:"required"` + } `json:"args"` + ReturnType struct { + Const string `json:"const"` + } `json:"returnType"` + } `json:"properties"` + Description string `json:"description"` +} + +type commonTypesFile struct { + Defs map[string]json.RawMessage `json:"$defs"` +} + +// Parsed data types. + +type Component struct { + Name string + Fields []Field + RequiredFields []Field + Checkable bool +} + +type Field struct { + Name string // JSON field name + GoName string // Exported Go field name + GoType string // Go type + JSONType string // for json tag + Required bool + Pointer bool // use pointer for optional fields + Enum []string +} + +type EnumType struct { + Name string + Values []EnumValue +} + +type EnumValue struct { + Name string // Go const name + Value string // JSON string value +} + +type FuncDef struct { + Name string // JSON name (camelCase) + GoName string // PascalCase + Args []FuncArg + ReturnType string // Go type for return + ReturnEnum string // JSON return type string + ReturnEnumPascal string // PascalCase version for const ref + Desc string +} + +type FuncArg struct { + Name string + GoName string + GoType string + Required bool +} + +// WrapDynamicValue returns a Go expression that wraps this arg into a DynamicValue. +func (a FuncArg) WrapDynamicValue() string { + switch a.GoType { + case "DynamicString": + return "dynamicStringToValue(" + a.Name + ")" + case "DynamicNumber": + return "dynamicNumberToValue(" + a.Name + ")" + case "DynamicBoolean": + return "dynamicBoolToValue(" + a.Name + ")" + case "DynamicValue": + return a.Name + case "string": + return "ValueString(" + a.Name + ")" + case "float64": + return "ValueNumber(" + a.Name + ")" + case "int": + return "ValueNumber(float64(" + a.Name + "))" + case "bool": + return "ValueBool(" + a.Name + ")" + case "[]DynamicBoolean": + return "dynamicBoolSliceToValue(" + a.Name + ")" + default: + return "ValueString(fmt.Sprint(" + a.Name + "))" + } +} + +type TemplateData struct { + Components []Component + Enums []EnumType + Icons []EnumValue + Functions []FuncDef + ReturnTypes []EnumValue + Wrappers []Wrapper + + Pkg string // Go package name for generated types (default "a2ui") + A2UIImport string // import path for the stable a2ui package + VersionImport string // import path for the generated version package + Stable bool // generate alias file +} + +type outputConfig struct { + A2UIDir string + A2UIBuildDir string + A2UIImport string + VersionImport string +} + +func resolveOutputConfig(out, module, a2uiDir, buildDir, a2uiImport, pkg string) (outputConfig, error) { + a2uiDir, err := cleanRelDir("-a2ui-dir", a2uiDir) + if err != nil { + return outputConfig{}, err + } + buildDir, err = cleanRelDir("-build-dir", buildDir) + if err != nil { + return outputConfig{}, err + } + + absOut, err := filepath.Abs(out) + if err != nil { + return outputConfig{}, err + } + if a2uiImport == "" { + modulePath, moduleRoot, err := resolveModule(module, absOut) + if err != nil { + return outputConfig{}, err + } + a2uiImport, err = importPathForDir(modulePath, moduleRoot, filepath.Join(absOut, a2uiDir)) + if err != nil { + return outputConfig{}, err + } + } + if err := checkImportPath("-a2ui-import", a2uiImport); err != nil { + return outputConfig{}, err + } + return outputConfig{ + A2UIDir: a2uiDir, + A2UIBuildDir: buildDir, + A2UIImport: a2uiImport, + VersionImport: joinImportPath(a2uiImport, pkg), + }, nil +} + +func cleanRelDir(flag, dir string) (string, error) { + if dir == "" { + return "", fmt.Errorf("%s is empty", flag) + } + dir = filepath.Clean(filepath.FromSlash(dir)) + if filepath.IsAbs(dir) || dir == ".." || strings.HasPrefix(dir, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("%s must be relative to -out", flag) + } + return dir, nil +} + +func resolveModule(module, out string) (modulePath, moduleRoot string, err error) { + modulePath = strings.TrimSpace(module) + root, found := findModuleRoot(out) + if found { + moduleRoot = root + } + if modulePath == "" { + if !found { + return "", "", fmt.Errorf("-module is required when -out is not inside a Go module") + } + modulePath, err = readModulePath(filepath.Join(root, "go.mod")) + if err != nil { + return "", "", err + } + } + if err := checkImportPath("-module", modulePath); err != nil { + return "", "", err + } + if moduleRoot == "" { + moduleRoot = out + } + return modulePath, moduleRoot, nil +} + +func findModuleRoot(dir string) (string, bool) { + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, true + } + parent := filepath.Dir(dir) + if parent == dir { + return "", false + } + dir = parent + } +} + +func readModulePath(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "module ") { + module := strings.TrimSpace(strings.TrimPrefix(line, "module ")) + if module == "" { + return "", fmt.Errorf("%s has empty module path", path) + } + return module, nil + } + } + return "", fmt.Errorf("%s has no module directive", path) +} + +func importPathForDir(modulePath, moduleRoot, dir string) (string, error) { + rel, err := filepath.Rel(moduleRoot, dir) + if err != nil { + return "", err + } + if rel == "." { + return modulePath, nil + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("output directory %s is outside module root %s", dir, moduleRoot) + } + return joinImportPath(modulePath, filepath.ToSlash(rel)), nil +} + +func joinImportPath(elem ...string) string { + var parts []string + for _, e := range elem { + e = strings.Trim(e, "/") + if e != "" && e != "." { + parts = append(parts, e) + } + } + return path.Join(parts...) +} + +func checkImportPath(flag, importPath string) error { + if importPath == "" { + return fmt.Errorf("%s is empty", flag) + } + if strings.ContainsAny(importPath, " \t\r\n") { + return fmt.Errorf("%s contains whitespace", flag) + } + return nil +} + +func generateSDK(out, module, a2uiDir, buildDir, a2uiImport, specRoot, sdkRoot string) error { + if sdkRoot == "" { + root, err := inferSDKRoot() + if err != nil { + return err + } + sdkRoot = root + } + if specRoot == "" { + specRoot = filepath.Join(sdkRoot, "..", "..", "specification") + } + if info, err := os.Stat(specRoot); err != nil || !info.IsDir() { + if err == nil { + err = fmt.Errorf("not a directory") + } + return fmt.Errorf("spec root %s: %w", specRoot, err) + } + + absOut, err := filepath.Abs(out) + if err != nil { + return err + } + modulePath, moduleRoot, err := resolveModule(module, absOut) + if err != nil { + return err + } + outConfig, err := resolveOutputConfig(out, module, a2uiDir, buildDir, a2uiImport, "v09") + if err != nil { + return err + } + sourceModule, err := readModulePath(filepath.Join(sdkRoot, "go.mod")) + if err != nil { + return err + } + + if err := copyStaticSDK(sdkRoot, specRoot, out, outConfig); err != nil { + return err + } + for _, v := range []struct { + spec string + pkg string + stable bool + }{ + {spec: "v0_10", pkg: "v010"}, + {spec: "v0_9_1", pkg: "v091"}, + {spec: "v0_9", pkg: "v09", stable: true}, + } { + if err := generateFromSchemas( + filepath.Join(specRoot, v.spec, "json"), + out, + v.pkg, + module, + a2uiDir, + buildDir, + a2uiImport, + v.stable, + ); err != nil { + return err + } + } + + if err := rewriteSDKImports(absOut, modulePath, moduleRoot, sourceModule, outConfig); err != nil { + return err + } + return gofmtTree(absOut) +} + +func inferSDKRoot() (string, error) { + _, file, _, ok := runtime.Caller(0) + if !ok { + return "", fmt.Errorf("cannot locate a2uigen source") + } + dir := filepath.Dir(file) + for { + if _, err := os.Stat(filepath.Join(dir, "cmd", "a2uigen")); err == nil { + if _, err := os.Stat(filepath.Join(dir, "a2ui", "v09")); err == nil { + return dir, nil + } + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("cannot infer Go SDK root from %s", file) + } + dir = parent + } +} + +func copyStaticSDK(sdkRoot, specRoot, out string, cfg outputConfig) error { + stableDir := filepath.Join(out, cfg.A2UIDir) + if err := os.MkdirAll(stableDir, 0o755); err != nil { + return err + } + for _, name := range []string{"doc.go", "example_test.go"} { + if err := copyFile(filepath.Join(sdkRoot, "a2ui", name), filepath.Join(stableDir, name)); err != nil { + return err + } + } + for _, v := range []struct { + spec string + pkg string + }{ + {spec: "v0_9", pkg: "v09"}, + {spec: "v0_9_1", pkg: "v091"}, + {spec: "v0_10", pkg: "v010"}, + } { + dst := filepath.Join(stableDir, v.pkg) + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := copyDir(filepath.Join(sdkRoot, "a2ui", v.pkg), dst, func(rel string, d fs.DirEntry) bool { + base := filepath.Base(rel) + if d.IsDir() { + return base == "testdata" + } + return base == "gen.go" || strings.HasPrefix(base, "zz_") + }); err != nil { + return err + } + examples := filepath.Join(dst, "testdata", v.spec, "catalogs", "basic", "examples") + if err := os.MkdirAll(examples, 0o755); err != nil { + return err + } + if err := copyDir(filepath.Join(specRoot, v.spec, "catalogs", "basic", "examples"), examples, nil); err != nil { + return err + } + } + + for _, pkg := range []string{"a2a", "a2uiadk", "a2uibuild", "a2uischema", "a2uistream"} { + dst := filepath.Join(out, pkg) + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := copyDir(filepath.Join(sdkRoot, pkg), dst, nil); err != nil { + return err + } + } + for _, pkg := range []string{"build", "schema", "stream"} { + if err := os.RemoveAll(filepath.Join(out, pkg)); err != nil { + return err + } + } + return nil +} + +func copyDir(src, dst string, skip func(rel string, d fs.DirEntry) bool) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + if skip != nil && skip(rel, d) { + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + target := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(target, 0o755) + } + return copyFile(path, target) + }) +} + +func copyFile(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + return os.WriteFile(dst, data, 0o644) +} + +func rewriteSDKImports(root, modulePath, moduleRoot, sourceModule string, cfg outputConfig) error { + helperImport := func(name string) (string, error) { + return importPathForDir(modulePath, moduleRoot, filepath.Join(root, name)) + } + buildImport, err := helperImport(cfg.A2UIBuildDir) + if err != nil { + return err + } + adkImport, err := helperImport("a2uiadk") + if err != nil { + return err + } + schemaImport, err := helperImport("a2uischema") + if err != nil { + return err + } + streamImport, err := helperImport("a2uistream") + if err != nil { + return err + } + + replacements := []struct{ old, new string }{ + {sourceModule + "/a2ui/v010", cfg.A2UIImport + "/v010"}, + {sourceModule + "/a2ui/v091", cfg.A2UIImport + "/v091"}, + {sourceModule + "/a2ui/v09", cfg.A2UIImport + "/v09"}, + {sourceModule + "/a2uiadk", adkImport}, + {sourceModule + "/a2uibuild", buildImport}, + {sourceModule + "/a2uischema", schemaImport}, + {sourceModule + "/a2uistream", streamImport}, + {sourceModule + "/a2ui", cfg.A2UIImport}, + {sourceModule, modulePath}, + } + return rewriteTree(root, replacements) +} + +func rewriteTree(root string, replacements []struct{ old, new string }) error { + return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if d.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + switch filepath.Ext(path) { + case ".go", ".md": + default: + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + text := string(data) + updated := text + for _, r := range replacements { + updated = strings.ReplaceAll(updated, r.old, r.new) + } + if updated == text { + return nil + } + return os.WriteFile(path, []byte(updated), 0o644) + }) +} + +func gofmtTree(root string) error { + return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if d.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + if filepath.Ext(path) != ".go" { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + formatted, err := format.Source(data) + if err != nil { + return fmt.Errorf("formatting %s: %w", path, err) + } + return os.WriteFile(path, formatted, 0o644) + }) +} + +// Wrapper describes a top-level list-wrapper schema such as +// server_to_client_list_wrapper.json, which envelopes a message list in +// {messages: [...]} for protocols that cannot carry a top-level array. +type Wrapper struct { + GoName string // Go type name, e.g. "ServerMessageListWrapper" + ItemGoType string // element Go type, e.g. "ServerMessage" + Field string // Go field name for the list, e.g. "Messages" + JSONField string // JSON field name, e.g. "messages" +} + +func parseCatalog(path string) (*catalogFile, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cat catalogFile + if err := json.Unmarshal(data, &cat); err != nil { + return nil, err + } + return &cat, nil +} + +func findBasicCatalog(schemaDir string) (string, error) { + candidates := []string{ + filepath.Join(schemaDir, "basic_catalog.json"), + filepath.Join(filepath.Dir(schemaDir), "catalogs", "basic", "catalog.json"), + } + for _, path := range candidates { + info, err := os.Stat(path) + if err == nil && !info.IsDir() { + return path, nil + } + } + return "", fmt.Errorf("basic catalog not found near %s", schemaDir) +} + +func parseCommonTypes(path string) (*commonTypesFile, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var ct commonTypesFile + if err := json.Unmarshal(data, &ct); err != nil { + return nil, err + } + return &ct, nil +} + +func buildTemplateData(cat *catalogFile, ct *commonTypesFile) (*TemplateData, error) { + td := &TemplateData{} + + // Parse components in a stable order. + compNames := sortedKeys(cat.Components) + enumSeen := map[string]*EnumType{} + + for _, name := range compNames { + raw := cat.Components[name] + comp, err := parseComponent(name, raw, enumSeen) + if err != nil { + return nil, err + } + td.Components = append(td.Components, comp) + } + + // Collect enums sorted by name. + for _, e := range enumSeen { + td.Enums = append(td.Enums, *e) + } + sort.Slice(td.Enums, func(i, j int) bool { + return td.Enums[i].Name < td.Enums[j].Name + }) + + // Parse icons. + icons, err := parseIcons(cat.Components["Icon"]) + if err != nil { + return nil, err + } + td.Icons = icons + + // Parse functions. + funcNames := sortedKeys(cat.Functions) + for _, name := range funcNames { + fd, err := parseFunction(name, cat.Functions[name]) + if err != nil { + return nil, err + } + td.Functions = append(td.Functions, fd) + } + + // Parse ReturnType enum from common_types. + returnTypes, err := parseReturnTypes(ct) + if err != nil { + return nil, err + } + td.ReturnTypes = returnTypes + + return td, nil +} + +func parseComponent(name string, raw json.RawMessage, enumSeen map[string]*EnumType) (Component, error) { + var cs componentSchema + if err := json.Unmarshal(raw, &cs); err != nil { + return Component{}, fmt.Errorf("parse component %s: %w", name, err) + } + + comp := Component{Name: name} + + for _, itemRaw := range cs.AllOf { + var item allOfItem + if err := json.Unmarshal(itemRaw, &item); err != nil { + return Component{}, fmt.Errorf("parse component %s allOf item: %w", name, err) + } + + if item.Ref != "" { + if strings.Contains(item.Ref, "Checkable") { + comp.Checkable = true + } + continue + } + + required := map[string]bool{} + for _, r := range item.Required { + required[r] = true + } + + propNames := sortedKeys(item.Properties) + for _, fname := range propNames { + prop := item.Properties[fname] + if fname == "component" { + continue + } + f := Field{ + Name: fname, + GoName: goFieldName(fname), + Required: required[fname], + JSONType: fname, + } + f.GoType = resolveGoType(prop, name, fname) + + // Handle enums. + enumValues := prop.Enum + if len(enumValues) > 0 { + enumTypeName := resolveEnumTypeName(name, fname, enumValues, enumSeen) + f.GoType = enumTypeName + } + + // Optional non-required fields with value types get pointer. + if !f.Required && needsPointer(f.GoType) { + f.Pointer = true + } + + comp.Fields = append(comp.Fields, f) + if f.Required { + comp.RequiredFields = append(comp.RequiredFields, f) + } + } + } + + return comp, nil +} + +// resolveEnumTypeName determines the Go enum type name. If another component +// already defined an enum with exactly the same values, they share the type. +func resolveEnumTypeName(compName, fieldName string, values []string, seen map[string]*EnumType) string { + // Check for shared enums by matching values. + sortedVals := make([]string, len(values)) + copy(sortedVals, values) + sort.Strings(sortedVals) + valKey := strings.Join(sortedVals, "|") + + // Mapping of known shared enum names by field name. + sharedNames := map[string]string{ + "justify": "LayoutJustify", + "align": "LayoutAlign", + } + + // Determine the type name. + typeName := compName + pascalCase(fieldName) + if shared, ok := sharedNames[fieldName]; ok { + // Check if existing enum with same field name has same values. + if existing, ok := seen[shared]; ok { + existingVals := make([]string, len(existing.Values)) + for i, v := range existing.Values { + existingVals[i] = v.Value + } + sort.Strings(existingVals) + if strings.Join(existingVals, "|") == valKey { + return shared + } + } + typeName = shared + } + + if _, ok := seen[typeName]; ok { + return typeName + } + + // Create enum values with prefixed const names. + et := &EnumType{Name: typeName} + for _, v := range values { + constName := typeName + pascalCase(v) + et.Values = append(et.Values, EnumValue{Name: constName, Value: v}) + } + seen[typeName] = et + return typeName +} + +func resolveGoType(prop propertySchema, compName, fieldName string) string { + if prop.Ref != "" { + return refToGoType(prop.Ref) + } + + // Handle allOf with a $ref (like DateTimeInput min/max). + if len(prop.AllOf) > 0 { + for _, item := range prop.AllOf { + if item.Ref != "" { + return refToGoType(item.Ref) + } + } + } + + // Handle oneOf for Icon name field. + if len(prop.OneOf) > 0 { + // Icon name has oneOf with enum string and object. + if fieldName == "name" && compName == "Icon" { + return "IconNameOrPath" + } + } + + switch prop.Type { + case "string": + return "string" + case "number": + return "float64" + case "integer": + return "int" + case "boolean": + return "bool" + case "array": + if prop.Items != nil { + if prop.Items.Type == "object" { + // Special inline struct types. + if compName == "Tabs" { + return "[]TabDef" + } + if compName == "ChoicePicker" { + return "[]ChoiceOption" + } + } + itemType := resolveGoType(*prop.Items, compName, fieldName) + return "[]" + itemType + } + return "[]any" + default: + return "any" + } +} + +func refToGoType(ref string) string { + // common_types.json#/$defs/DynamicString → DynamicString + if idx := strings.LastIndex(ref, "/"); idx >= 0 { + typeName := ref[idx+1:] + switch typeName { + case "ComponentId": + return "string" + default: + return typeName + } + } + return "any" +} + +func needsPointer(goType string) bool { + switch goType { + case "bool", "float64", "int", + "DynamicString", "DynamicNumber", "DynamicBoolean", + "DynamicStringList", "DynamicValue", "Action", "IconNameOrPath": + return true + default: + return false + } +} + +// parseWrappers reads every *_list_wrapper.json schema in dir and resolves it +// down to a Wrapper describing the {messages: [ItemGoType]} envelope it +// represents. The generator emits one Go struct per Wrapper. +func parseWrappers(dir string) ([]Wrapper, error) { + matches, err := filepath.Glob(filepath.Join(dir, "*_list_wrapper.json")) + if err != nil { + return nil, err + } + sort.Strings(matches) + var out []Wrapper + for _, path := range matches { + w, err := parseWrapper(dir, path) + if err != nil { + return nil, fmt.Errorf("wrapper %s: %w", filepath.Base(path), err) + } + out = append(out, w) + } + return out, nil +} + +type wrapperSchema struct { + Title string `json:"title"` + Properties map[string]propertySchema `json:"properties"` + Required []string `json:"required"` +} + +func parseWrapper(dir, path string) (Wrapper, error) { + raw, err := os.ReadFile(path) + if err != nil { + return Wrapper{}, err + } + var ws wrapperSchema + if err := json.Unmarshal(raw, &ws); err != nil { + return Wrapper{}, err + } + if len(ws.Properties) != 1 { + return Wrapper{}, fmt.Errorf("expected one property, got %d", len(ws.Properties)) + } + var jsonField string + var prop propertySchema + for k, v := range ws.Properties { + jsonField = k + prop = v + } + if prop.Ref == "" { + return Wrapper{}, fmt.Errorf("property %q has no $ref", jsonField) + } + + // Resolve the list schema: list refs have "items.$ref" pointing to the + // element schema. The element schema's file name encodes the Go type name. + listPath := filepath.Join(dir, prop.Ref) + listRaw, err := os.ReadFile(listPath) + if err != nil { + return Wrapper{}, fmt.Errorf("reading list schema: %w", err) + } + var list propertySchema + if err := json.Unmarshal(listRaw, &list); err != nil { + return Wrapper{}, err + } + if list.Items == nil || list.Items.Ref == "" { + return Wrapper{}, fmt.Errorf("list schema %s has no items.$ref", prop.Ref) + } + itemGoType := messageGoTypeFromRef(list.Items.Ref) + if itemGoType == "" { + return Wrapper{}, fmt.Errorf("cannot map %q to a Go type", list.Items.Ref) + } + + base := strings.TrimSuffix(filepath.Base(path), ".json") + return Wrapper{ + GoName: wrapperGoName(base, itemGoType), + ItemGoType: itemGoType, + Field: pascalCase(jsonField), + JSONField: jsonField, + }, nil +} + +// messageGoTypeFromRef maps a ref like "server_to_client.json" to the +// hand-written Go message type "ServerMessage" that represents it. +func messageGoTypeFromRef(ref string) string { + switch filepath.Base(ref) { + case "server_to_client.json": + return "ServerMessage" + case "client_to_server.json": + return "ClientMessage" + } + return "" +} + +// wrapperGoName derives the Go type name for a wrapper schema. For the +// message list wrappers the name is "{Item}ListWrapper". +func wrapperGoName(base, itemGoType string) string { + switch base { + case "server_to_client_list_wrapper", "client_to_server_list_wrapper": + return itemGoType + "ListWrapper" + } + return pascalCase(base) +} + +func qualifyBuilderType(goType string) string { + if strings.HasPrefix(goType, "[]") { + return "[]" + qualifyBuilderType(strings.TrimPrefix(goType, "[]")) + } + switch goType { + case "string", "bool", "int", "float64", "any": + return goType + default: + return "a2ui." + goType + } +} + +func parseIcons(raw json.RawMessage) ([]EnumValue, error) { + var cs componentSchema + if err := json.Unmarshal(raw, &cs); err != nil { + return nil, fmt.Errorf("parse icons: %w", err) + } + + for _, itemRaw := range cs.AllOf { + var item allOfItem + if err := json.Unmarshal(itemRaw, &item); err != nil { + return nil, fmt.Errorf("parse icons allOf item: %w", err) + } + if item.Properties == nil { + continue + } + nameProp, ok := item.Properties["name"] + if !ok { + continue + } + if len(nameProp.OneOf) > 0 { + for _, opt := range nameProp.OneOf { + if len(opt.Enum) > 0 { + var icons []EnumValue + for _, v := range opt.Enum { + icons = append(icons, EnumValue{ + Name: "Icon" + pascalCase(v), + Value: v, + }) + } + return icons, nil + } + } + } + } + return nil, nil +} + +func parseFunction(name string, raw json.RawMessage) (FuncDef, error) { + var fs functionSchema + if err := json.Unmarshal(raw, &fs); err != nil { + return FuncDef{}, fmt.Errorf("parse function %s: %w", name, err) + } + + fd := FuncDef{ + Name: name, + GoName: pascalCase(name), + ReturnEnum: fs.Properties.ReturnType.Const, + ReturnEnumPascal: pascalCase(fs.Properties.ReturnType.Const), + Desc: fs.Description, + } + + switch fs.Properties.ReturnType.Const { + case "boolean": + fd.ReturnType = "DynamicBoolean" + case "string": + fd.ReturnType = "DynamicString" + case "number": + fd.ReturnType = "DynamicNumber" + case "void": + fd.ReturnType = "Action" + default: + fd.ReturnType = "DynamicValue" + } + + argNames := sortedKeys(fs.Properties.Args.Properties) + reqArgs := map[string]bool{} + for _, r := range fs.Properties.Args.Required { + reqArgs[r] = true + } + + for _, aname := range argNames { + aprop := fs.Properties.Args.Properties[aname] + arg := FuncArg{ + Name: aname, + GoName: pascalCase(aname), + GoType: resolveGoType(aprop, "", aname), + Required: reqArgs[aname], + } + // For the "required" function, the "value" arg has no type/ref, use DynamicValue. + if arg.GoType == "any" { + arg.GoType = "DynamicValue" + } + // And/Or values are []DynamicBoolean + if aprop.Type == "array" && aprop.Items != nil && aprop.Items.Ref != "" { + arg.GoType = "[]" + refToGoType(aprop.Items.Ref) + } + fd.Args = append(fd.Args, arg) + } + + return fd, nil +} + +func parseReturnTypes(ct *commonTypesFile) ([]EnumValue, error) { + var fc struct { + Properties struct { + ReturnType struct { + Enum []string `json:"enum"` + } `json:"returnType"` + } `json:"properties"` + } + if raw, ok := ct.Defs["FunctionCall"]; ok { + if err := json.Unmarshal(raw, &fc); err != nil { + return nil, fmt.Errorf("parse FunctionCall returnType: %w", err) + } + } + var vals []EnumValue + for _, v := range fc.Properties.ReturnType.Enum { + vals = append(vals, EnumValue{ + Name: "ReturnType" + pascalCase(v), + Value: v, + }) + } + return vals, nil +} + +// Template helper functions. + +func pascalCase(s string) string { + if s == "" { + return "" + } + acronyms := map[string]string{ + "url": "URL", "id": "ID", "html": "HTML", "css": "CSS", + "http": "HTTP", "https": "HTTPS", "api": "API", "uri": "URI", + } + var out strings.Builder + for _, word := range identifierWords(s) { + if upper, ok := acronyms[strings.ToLower(word)]; ok { + out.WriteString(upper) + continue + } + runes := []rune(word) + runes[0] = unicode.ToUpper(runes[0]) + out.WriteString(string(runes)) + } + return out.String() +} + +func identifierWords(s string) []string { + runes := []rune(s) + var words []string + start := 0 + for i := 0; i < len(runes); i++ { + if !unicode.IsLetter(runes[i]) && !unicode.IsDigit(runes[i]) { + if start < i { + words = append(words, string(runes[start:i])) + } + start = i + 1 + continue + } + if i > start && unicode.IsUpper(runes[i]) && (unicode.IsLower(runes[i-1]) || unicode.IsDigit(runes[i-1])) { + words = append(words, string(runes[start:i])) + start = i + } + } + if start < len(runes) { + words = append(words, string(runes[start:])) + } + return words +} + +func goFieldName(s string) string { + return pascalCase(s) +} + +func sortedKeys[V any](m map[string]V) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) + return keys +} diff --git a/agent_sdks/go/cmd/a2uigen/main_test.go b/agent_sdks/go/cmd/a2uigen/main_test.go new file mode 100644 index 0000000000..3a89613aa7 --- /dev/null +++ b/agent_sdks/go/cmd/a2uigen/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveOutputConfigDefaultLayout(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/a2ui\n"), 0o644); err != nil { + t.Fatal(err) + } + + cfg, err := resolveOutputConfig(dir, "", "a2ui", "a2uibuild", "", "v09") + if err != nil { + t.Fatal(err) + } + if cfg.A2UIDir != "a2ui" { + t.Fatalf("A2UIDir = %q, want a2ui", cfg.A2UIDir) + } + if cfg.A2UIBuildDir != "a2uibuild" { + t.Fatalf("A2UIBuildDir = %q, want a2uibuild", cfg.A2UIBuildDir) + } + if cfg.A2UIImport != "example.com/a2ui/a2ui" { + t.Fatalf("A2UIImport = %q, want example.com/a2ui/a2ui", cfg.A2UIImport) + } + if cfg.VersionImport != "example.com/a2ui/a2ui/v09" { + t.Fatalf("VersionImport = %q, want example.com/a2ui/a2ui/v09", cfg.VersionImport) + } +} + +func TestResolveOutputConfigRootLayout(t *testing.T) { + dir := t.TempDir() + + cfg, err := resolveOutputConfig(dir, "example.com/root", ".", "a2uibuild", "", "v09") + if err != nil { + t.Fatal(err) + } + if cfg.A2UIDir != "." { + t.Fatalf("A2UIDir = %q, want .", cfg.A2UIDir) + } + if cfg.A2UIImport != "example.com/root" { + t.Fatalf("A2UIImport = %q, want example.com/root", cfg.A2UIImport) + } + if cfg.VersionImport != "example.com/root/v09" { + t.Fatalf("VersionImport = %q, want example.com/root/v09", cfg.VersionImport) + } +} + +func TestResolveOutputConfigExplicitA2UIImport(t *testing.T) { + dir := t.TempDir() + + cfg, err := resolveOutputConfig(dir, "", "generated/a2ui", "generated/a2uibuild", "example.com/custom/a2ui", "v010") + if err != nil { + t.Fatal(err) + } + if cfg.A2UIImport != "example.com/custom/a2ui" { + t.Fatalf("A2UIImport = %q, want example.com/custom/a2ui", cfg.A2UIImport) + } + if cfg.VersionImport != "example.com/custom/a2ui/v010" { + t.Fatalf("VersionImport = %q, want example.com/custom/a2ui/v010", cfg.VersionImport) + } +} diff --git a/agent_sdks/go/cmd/a2uigen/templates/types.go.txtar b/agent_sdks/go/cmd/a2uigen/templates/types.go.txtar new file mode 100644 index 0000000000..453b088134 --- /dev/null +++ b/agent_sdks/go/cmd/a2uigen/templates/types.go.txtar @@ -0,0 +1,358 @@ +-- zz_enum.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} + +// ReturnType is the expected return type of a function call. +type ReturnType string + +const ({{range .ReturnTypes}} + {{.Name}} ReturnType = "{{.Value}}"{{end}} +) + +// IconName identifies a built-in icon. +type IconName string +{{range .Enums}} +// {{.Name}} defines the allowed values for the {{.Name}} enum. +type {{.Name}} string +{{$enumName := .Name}} +const ({{range .Values}} + {{.Name}} {{$enumName}} = "{{.Value}}"{{end}} +) +{{- end}} +-- zz_icon.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} + +// Well-known icon names. +const ({{range .Icons}} + {{.Name}} IconName = "{{.Value}}"{{end}} +) +-- zz_component.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} + +// TabDef defines a tab within a [Tabs] component. +type TabDef struct { + Title DynamicString `json:"title"` + Child string `json:"child"` +} + +// ChoiceOption defines a selectable option within a [ChoicePicker] component. +type ChoiceOption struct { + Label DynamicString `json:"label"` + Value string `json:"value"` +} +{{range .Components}} +// {{.Name}}Component holds the component-specific fields for a {{.Name}}. +type {{.Name}}Component struct { +{{- range .Fields}} + {{.GoName}} {{if .Pointer}}*{{end}}{{.GoType}} `json:"{{.JSONType}}{{if not .Required}},omitempty{{end}}"` +{{- end}} +} +{{end}} +-- zz_component_marshal.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON encodes a [Component] as a flat JSON object with the "component" +// discriminator, common fields, and component-specific fields merged together. +func (c Component) MarshalJSON() ([]byte, error) { + type common struct { + ComponentType string `json:"component"` + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + componentType, specific, count := c.componentData() + switch count { + case 0: + return nil, fmt.Errorf("a2ui: component has no concrete type set") + case 1: + default: + return nil, fmt.Errorf("a2ui: component has multiple concrete types set") + } + cm := common{ + ComponentType: componentType, + ID: c.ID, + Accessibility: c.Accessibility, + Weight: c.Weight, + Checks: c.Checks, + } + + commonBytes, err := json.Marshal(cm) + if err != nil { + return nil, err + } + + specificBytes, err := json.Marshal(specific) + if err != nil { + return nil, err + } + + // Merge: overwrite common with specific fields. + if len(specificBytes) <= 2 { // "{}" or empty + return commonBytes, nil + } + // Remove leading '{' from specific, append to common. + merged := make([]byte, 0, len(commonBytes)+len(specificBytes)) + merged = append(merged, commonBytes[:len(commonBytes)-1]...) // drop trailing '}' + merged = append(merged, ',') + merged = append(merged, specificBytes[1:]...) // drop leading '{' + return merged, nil +} + +// UnmarshalJSON decodes a flat JSON object into a [Component], using the +// "component" field as the discriminator. +func (c *Component) UnmarshalJSON(data []byte) error { + // Read the discriminator. + var disc struct { + ComponentType string `json:"component"` + } + if err := json.Unmarshal(data, &disc); err != nil { + return err + } + + // Unmarshal common fields. + type commonOnly struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + var cm commonOnly + if err := json.Unmarshal(data, &cm); err != nil { + return err + } + *c = Component{} + c.ID = cm.ID + c.Accessibility = cm.Accessibility + c.Weight = cm.Weight + c.Checks = cm.Checks + + // Unmarshal component-specific fields. + switch disc.ComponentType { +{{- range .Components}} + case "{{.Name}}": + var v {{.Name}}Component + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.{{.Name}} = &v +{{- end}} + default: + return fmt.Errorf("unknown component type: %s", disc.ComponentType) + } + return nil +} +-- zz_function.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} + +{{range .Functions}} +// {{.GoName}} creates a function call for "{{.Name}}". +// {{.Desc}} +func {{.GoName}}({{range $i, $a := .Args}}{{if $i}}, {{end}}{{$a.Name}} {{$a.GoType}}{{end}}) {{.ReturnType}} { +{{- if eq .ReturnType "Action"}} + return Action{FunctionCall: &FunctionCall{ + Call: "{{.Name}}", + Args: map[string]any{ +{{- range .Args}} + "{{.Name}}": {{.Name}}, +{{- end}} + }, + ReturnType: ReturnType{{.ReturnEnumPascal}}, + }} +{{- else}} + return {{.ReturnType}}{FunctionCall: &FunctionCall{ + Call: "{{.Name}}", + Args: map[string]any{ +{{- range .Args}} + "{{.Name}}": {{.Name}}, +{{- end}} + }, + ReturnType: ReturnType{{.ReturnEnumPascal}}, + }} +{{- end}} +} +{{end}} +-- zz_wrapper.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package {{.Pkg}} +{{range .Wrappers}} +// {{.GoName}} wraps a list of [{{.ItemGoType}}] in a {"{{.JSONField}}": [...]} +// envelope for transports that require a top-level JSON object. +type {{.GoName}} struct { + {{.Field}} []{{.ItemGoType}} `json:"{{.JSONField}}"` +} +{{end}} +-- zz_builders.go -- +// Code generated by a2uigen; DO NOT EDIT. + +// Package a2uibuild provides convenience constructors for A2UI components. +package a2uibuild + +import "{{.A2UIImport}}" +{{range .Components}} +// {{.Name}} creates a new [a2ui.Component] of type {{.Name}} with the given id. +func {{.Name}}(id string{{range .RequiredFields}}, {{.Name}} {{qualifyBuilderType .GoType}}{{end}}) a2ui.Component { + return a2ui.Component{ + ID: id, + {{.Name}}: &a2ui.{{.Name}}Component{ +{{- range .RequiredFields}} + {{.GoName}}: {{.Name}}, +{{- end}} + }, + } +} +{{end}} +-- a2ui.go -- +// Code generated by a2uigen; DO NOT EDIT. + +package a2ui + +import "{{.VersionImport}}" + +// Version re-exports the protocol version from the active implementation. +const Version = {{.Pkg}}.Version + +// Hand-written message types. +type ( + ServerMessage = {{.Pkg}}.ServerMessage + ClientMessage = {{.Pkg}}.ClientMessage +) + +// Hand-written server-to-client message types. +type ( + CreateSurface = {{.Pkg}}.CreateSurface + UpdateComponents = {{.Pkg}}.UpdateComponents + UpdateDataModel = {{.Pkg}}.UpdateDataModel + DeleteSurface = {{.Pkg}}.DeleteSurface +) + +// Hand-written client-to-server message types. +type ( + ActionEvent = {{.Pkg}}.ActionEvent + ClientError = {{.Pkg}}.ClientError +) + +// Hand-written common types. +type ( + Component = {{.Pkg}}.Component + DynamicString = {{.Pkg}}.DynamicString + DynamicNumber = {{.Pkg}}.DynamicNumber + DynamicBoolean = {{.Pkg}}.DynamicBoolean + DynamicStringList = {{.Pkg}}.DynamicStringList + DynamicValue = {{.Pkg}}.DynamicValue + DataBinding = {{.Pkg}}.DataBinding + FunctionCall = {{.Pkg}}.FunctionCall + ChildList = {{.Pkg}}.ChildList + ChildTemplate = {{.Pkg}}.ChildTemplate + CheckRule = {{.Pkg}}.CheckRule + AccessibilityAttributes = {{.Pkg}}.AccessibilityAttributes + Theme = {{.Pkg}}.Theme + Action = {{.Pkg}}.Action + EventAction = {{.Pkg}}.EventAction + IconNameOrPath = {{.Pkg}}.IconNameOrPath +) + +// Capability types. +type ( + ClientCapabilities = {{.Pkg}}.ClientCapabilities + ClientCapabilitiesV09 = {{.Pkg}}.ClientCapabilitiesV09 + ServerCapabilities = {{.Pkg}}.ServerCapabilities + ServerCapabilitiesV09 = {{.Pkg}}.ServerCapabilitiesV09 + CatalogDef = {{.Pkg}}.CatalogDef + FunctionDefinition = {{.Pkg}}.FunctionDefinition + ClientDataModel = {{.Pkg}}.ClientDataModel +) + +// Generated component types. +type ( +{{- range .Components}} + {{.Name}}Component = {{$.Pkg}}.{{.Name}}Component +{{- end}} +) + +// Generated inline struct types. +type ( + TabDef = {{.Pkg}}.TabDef + ChoiceOption = {{.Pkg}}.ChoiceOption +) + +// Generated message list-wrapper types. +type ( +{{- range .Wrappers}} + {{.GoName}} = {{$.Pkg}}.{{.GoName}} +{{- end}} +) + +// Generated enum types. +type ( + ReturnType = {{.Pkg}}.ReturnType + IconName = {{.Pkg}}.IconName +{{- range .Enums}} + {{.Name}} = {{$.Pkg}}.{{.Name}} +{{- end}} +) + +// ReturnType constants. +const ( +{{- range .ReturnTypes}} + {{.Name}} = {{$.Pkg}}.{{.Name}} +{{- end}} +) + +// Icon constants. +const ( +{{- range .Icons}} + {{.Name}} = {{$.Pkg}}.{{.Name}} +{{- end}} +) + +// Enum constants. +const ( +{{- range .Enums}}{{range .Values}} + {{.Name}} = {{$.Pkg}}.{{.Name}} +{{- end}}{{end}} +) + +// Hand-written Dynamic* constructors. +var ( + StringLiteral = {{.Pkg}}.StringLiteral + StringBinding = {{.Pkg}}.StringBinding + StringFunc = {{.Pkg}}.StringFunc + NumberLiteral = {{.Pkg}}.NumberLiteral + NumberBinding = {{.Pkg}}.NumberBinding + NumberFunc = {{.Pkg}}.NumberFunc + BoolLiteral = {{.Pkg}}.BoolLiteral + BoolBinding = {{.Pkg}}.BoolBinding + BoolFunc = {{.Pkg}}.BoolFunc + StringListLiteral = {{.Pkg}}.StringListLiteral + StringListBinding = {{.Pkg}}.StringListBinding + StringListFunc = {{.Pkg}}.StringListFunc + ValueString = {{.Pkg}}.ValueString + ValueNumber = {{.Pkg}}.ValueNumber + ValueBool = {{.Pkg}}.ValueBool + ValueArray = {{.Pkg}}.ValueArray + ValueBinding = {{.Pkg}}.ValueBinding + ValueFunc = {{.Pkg}}.ValueFunc +) + +// Generated function constructors. +var ( +{{- range .Functions}} + {{.GoName}} = {{$.Pkg}}.{{.GoName}} +{{- end}} +) diff --git a/agent_sdks/go/go.mod b/agent_sdks/go/go.mod new file mode 100644 index 0000000000..c4d0666423 --- /dev/null +++ b/agent_sdks/go/go.mod @@ -0,0 +1,5 @@ +module github.com/a2ui-project/a2ui/agent_sdks/go + +go 1.25.0 + +require golang.org/x/tools v0.43.0 diff --git a/agent_sdks/go/go.sum b/agent_sdks/go/go.sum new file mode 100644 index 0000000000..66c1f4209b --- /dev/null +++ b/agent_sdks/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= From ec8912d1552b190da5f68a528da618d37a70ece6 Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Thu, 4 Jun 2026 18:19:17 -0700 Subject: [PATCH 2/2] agent_sdks/go: add generated SDK output Check in the Go SDK output produced by cmd/a2uigen, including versioned v0.9, v0.9.1, and v0.10 packages, schema validation helpers, streaming helpers, A2A and ADK support, and builder conveniences. Also add the SDK generation integration test now that the checked-in support tree exists. --- agent_sdks/go/a2a/a2a.go | 271 ++++ agent_sdks/go/a2a/a2a_test.go | 116 ++ agent_sdks/go/a2a/conformance_test.go | 219 +++ agent_sdks/go/a2a/doc.go | 7 + agent_sdks/go/a2a/example_test.go | 39 + agent_sdks/go/a2ui/a2ui.go | 272 ++++ agent_sdks/go/a2ui/doc.go | 10 + agent_sdks/go/a2ui/example_test.go | 34 + agent_sdks/go/a2ui/v010/capabilities.go | 61 + agent_sdks/go/a2ui/v010/common.go | 68 + agent_sdks/go/a2ui/v010/common_json.go | 173 +++ agent_sdks/go/a2ui/v010/common_test.go | 63 + agent_sdks/go/a2ui/v010/component.go | 110 ++ agent_sdks/go/a2ui/v010/component_test.go | 254 +++ agent_sdks/go/a2ui/v010/doc.go | 2 + agent_sdks/go/a2ui/v010/dynamic.go | 120 ++ agent_sdks/go/a2ui/v010/dynamic_json.go | 239 +++ agent_sdks/go/a2ui/v010/dynamic_test.go | 308 ++++ agent_sdks/go/a2ui/v010/example_test.go | 63 + agent_sdks/go/a2ui/v010/gen.go | 3 + agent_sdks/go/a2ui/v010/message.go | 105 ++ agent_sdks/go/a2ui/v010/message_json.go | 166 ++ agent_sdks/go/a2ui/v010/message_test.go | 552 +++++++ .../basic/examples/01_flight-status.json | 201 +++ .../basic/examples/02_email-compose.json | 185 +++ .../basic/examples/03_calendar-day.json | 166 ++ .../basic/examples/04_weather-current.json | 168 ++ .../basic/examples/05_product-card.json | 151 ++ .../basic/examples/06_music-player.json | 165 ++ .../catalogs/basic/examples/07_task-card.json | 107 ++ .../basic/examples/08_user-profile.json | 190 +++ .../basic/examples/09_login-form.json | 214 +++ .../examples/10_notification-permission.json | 105 ++ .../basic/examples/11_purchase-complete.json | 169 ++ .../basic/examples/12_chat-message.json | 144 ++ .../basic/examples/13_coffee-order.json | 253 +++ .../basic/examples/14_sports-player.json | 177 +++ .../basic/examples/15_account-balance.json | 126 ++ .../basic/examples/16_workout-summary.json | 160 ++ .../basic/examples/17_event-detail.json | 144 ++ .../basic/examples/18_track-list.json | 152 ++ .../basic/examples/19_software-purchase.json | 194 +++ .../basic/examples/20_restaurant-card.json | 140 ++ .../basic/examples/21_shipping-status.json | 137 ++ .../basic/examples/22_credit-card.json | 117 ++ .../basic/examples/23_step-counter.json | 149 ++ .../basic/examples/24_recipe-card.json | 204 +++ .../basic/examples/25_contact-card.json | 175 +++ .../basic/examples/26_podcast-episode.json | 123 ++ .../basic/examples/27_stats-card.json | 106 ++ .../basic/examples/28_countdown-timer.json | 135 ++ .../basic/examples/29_movie-card.json | 156 ++ .../examples/30_live-invitation-builder.json | 205 +++ .../examples/31_incremental-dashboard.json | 128 ++ .../examples/32_advanced-form-validator.json | 166 ++ .../examples/33_financial-data-grid.json | 171 ++ .../examples/34_child-list-template.json | 80 + .../basic/examples/35_markdown-text.json | 44 + .../catalogs/basic/examples/36_modal.json | 65 + agent_sdks/go/a2ui/v010/zz_component.go | 138 ++ .../go/a2ui/v010/zz_component_marshal.go | 200 +++ agent_sdks/go/a2ui/v010/zz_enum.go | 128 ++ agent_sdks/go/a2ui/v010/zz_function.go | 188 +++ agent_sdks/go/a2ui/v010/zz_icon.go | 66 + agent_sdks/go/a2ui/v010/zz_wrapper.go | 15 + agent_sdks/go/a2ui/v09/capabilities.go | 50 + agent_sdks/go/a2ui/v09/common.go | 67 + agent_sdks/go/a2ui/v09/common_json.go | 183 +++ agent_sdks/go/a2ui/v09/common_test.go | 51 + agent_sdks/go/a2ui/v09/component.go | 110 ++ agent_sdks/go/a2ui/v09/component_test.go | 254 +++ agent_sdks/go/a2ui/v09/doc.go | 2 + agent_sdks/go/a2ui/v09/dynamic.go | 120 ++ agent_sdks/go/a2ui/v09/dynamic_json.go | 239 +++ agent_sdks/go/a2ui/v09/dynamic_test.go | 308 ++++ agent_sdks/go/a2ui/v09/example_test.go | 63 + agent_sdks/go/a2ui/v09/gen.go | 3 + agent_sdks/go/a2ui/v09/message.go | 71 + agent_sdks/go/a2ui/v09/message_json.go | 76 + agent_sdks/go/a2ui/v09/message_test.go | 320 ++++ .../basic/examples/01_flight-status.json | 201 +++ .../basic/examples/02_email-compose.json | 185 +++ .../basic/examples/03_calendar-day.json | 166 ++ .../basic/examples/04_weather-current.json | 168 ++ .../basic/examples/05_product-card.json | 151 ++ .../basic/examples/06_music-player.json | 165 ++ .../catalogs/basic/examples/07_task-card.json | 107 ++ .../basic/examples/08_user-profile.json | 190 +++ .../basic/examples/09_login-form.json | 214 +++ .../examples/10_notification-permission.json | 105 ++ .../basic/examples/11_purchase-complete.json | 169 ++ .../basic/examples/12_chat-message.json | 144 ++ .../basic/examples/13_coffee-order.json | 253 +++ .../basic/examples/14_sports-player.json | 177 +++ .../basic/examples/15_account-balance.json | 126 ++ .../basic/examples/16_workout-summary.json | 160 ++ .../basic/examples/17_event-detail.json | 144 ++ .../basic/examples/18_track-list.json | 152 ++ .../basic/examples/19_software-purchase.json | 194 +++ .../basic/examples/20_restaurant-card.json | 140 ++ .../basic/examples/21_shipping-status.json | 137 ++ .../basic/examples/22_credit-card.json | 117 ++ .../basic/examples/23_step-counter.json | 149 ++ .../basic/examples/24_recipe-card.json | 204 +++ .../basic/examples/25_contact-card.json | 175 +++ .../basic/examples/26_podcast-episode.json | 123 ++ .../basic/examples/27_stats-card.json | 106 ++ .../basic/examples/28_countdown-timer.json | 135 ++ .../basic/examples/29_movie-card.json | 156 ++ .../examples/30_live-invitation-builder.json | 205 +++ .../examples/31_incremental-dashboard.json | 128 ++ .../examples/32_advanced-form-validator.json | 166 ++ .../examples/33_financial-data-grid.json | 171 ++ .../examples/34_child-list-template.json | 80 + .../basic/examples/35_markdown-text.json | 44 + .../catalogs/basic/examples/36_modal.json | 65 + agent_sdks/go/a2ui/v09/zz_component.go | 136 ++ .../go/a2ui/v09/zz_component_marshal.go | 200 +++ agent_sdks/go/a2ui/v09/zz_enum.go | 129 ++ agent_sdks/go/a2ui/v09/zz_function.go | 188 +++ agent_sdks/go/a2ui/v09/zz_icon.go | 66 + agent_sdks/go/a2ui/v09/zz_wrapper.go | 15 + agent_sdks/go/a2ui/v091/capabilities.go | 50 + agent_sdks/go/a2ui/v091/common.go | 67 + agent_sdks/go/a2ui/v091/common_json.go | 183 +++ agent_sdks/go/a2ui/v091/common_test.go | 51 + agent_sdks/go/a2ui/v091/component.go | 110 ++ agent_sdks/go/a2ui/v091/component_test.go | 254 +++ agent_sdks/go/a2ui/v091/doc.go | 2 + agent_sdks/go/a2ui/v091/dynamic.go | 120 ++ agent_sdks/go/a2ui/v091/dynamic_json.go | 239 +++ agent_sdks/go/a2ui/v091/dynamic_test.go | 308 ++++ agent_sdks/go/a2ui/v091/example_test.go | 63 + agent_sdks/go/a2ui/v091/gen.go | 3 + agent_sdks/go/a2ui/v091/message.go | 71 + agent_sdks/go/a2ui/v091/message_json.go | 76 + agent_sdks/go/a2ui/v091/message_test.go | 320 ++++ .../basic/examples/01_flight-status.json | 201 +++ .../basic/examples/02_email-compose.json | 185 +++ .../basic/examples/03_calendar-day.json | 166 ++ .../basic/examples/04_weather-current.json | 168 ++ .../basic/examples/05_product-card.json | 151 ++ .../basic/examples/06_music-player.json | 165 ++ .../catalogs/basic/examples/07_task-card.json | 107 ++ .../basic/examples/08_user-profile.json | 190 +++ .../basic/examples/09_login-form.json | 214 +++ .../examples/10_notification-permission.json | 105 ++ .../basic/examples/11_purchase-complete.json | 169 ++ .../basic/examples/12_chat-message.json | 144 ++ .../basic/examples/13_coffee-order.json | 253 +++ .../basic/examples/14_sports-player.json | 177 +++ .../basic/examples/15_account-balance.json | 126 ++ .../basic/examples/16_workout-summary.json | 160 ++ .../basic/examples/17_event-detail.json | 144 ++ .../basic/examples/18_track-list.json | 152 ++ .../basic/examples/19_software-purchase.json | 194 +++ .../basic/examples/20_restaurant-card.json | 140 ++ .../basic/examples/21_shipping-status.json | 137 ++ .../basic/examples/22_credit-card.json | 117 ++ .../basic/examples/23_step-counter.json | 149 ++ .../basic/examples/24_recipe-card.json | 204 +++ .../basic/examples/25_contact-card.json | 175 +++ .../basic/examples/26_podcast-episode.json | 123 ++ .../basic/examples/27_stats-card.json | 106 ++ .../basic/examples/28_countdown-timer.json | 135 ++ .../basic/examples/29_movie-card.json | 156 ++ .../examples/30_live-invitation-builder.json | 205 +++ .../examples/31_incremental-dashboard.json | 128 ++ .../examples/32_advanced-form-validator.json | 166 ++ .../examples/33_financial-data-grid.json | 171 ++ .../examples/34_child-list-template.json | 80 + .../basic/examples/35_markdown-text.json | 44 + .../catalogs/basic/examples/36_modal.json | 65 + agent_sdks/go/a2ui/v091/zz_component.go | 136 ++ .../go/a2ui/v091/zz_component_marshal.go | 200 +++ agent_sdks/go/a2ui/v091/zz_enum.go | 129 ++ agent_sdks/go/a2ui/v091/zz_function.go | 188 +++ agent_sdks/go/a2ui/v091/zz_icon.go | 66 + agent_sdks/go/a2ui/v091/zz_wrapper.go | 15 + agent_sdks/go/a2uiadk/doc.go | 2 + agent_sdks/go/a2uiadk/example_test.go | 25 + agent_sdks/go/a2uiadk/tool.go | 105 ++ agent_sdks/go/a2uiadk/tool_test.go | 93 ++ agent_sdks/go/a2uibuild/children.go | 8 + agent_sdks/go/a2uibuild/doc.go | 6 + agent_sdks/go/a2uibuild/example_test.go | 23 + agent_sdks/go/a2uibuild/surface.go | 93 ++ agent_sdks/go/a2uibuild/surface_test.go | 58 + agent_sdks/go/a2uibuild/zz_builders.go | 189 +++ agent_sdks/go/a2uischema/assets.go | 41 + agent_sdks/go/a2uischema/catalog.go | 433 ++++++ .../go/a2uischema/catalog_conformance_test.go | 121 ++ agent_sdks/go/a2uischema/constants.go | 24 + agent_sdks/go/a2uischema/doc.go | 2 + agent_sdks/go/a2uischema/error.go | 44 + agent_sdks/go/a2uischema/manager.go | 557 +++++++ agent_sdks/go/a2uischema/provider.go | 48 + agent_sdks/go/a2uischema/schema_test.go | 309 ++++ .../schemas/v0_10/basic_catalog.json | 1189 ++++++++++++++ .../schemas/v0_10/basic_catalog_rules.txt | 5 + .../schemas/v0_10/common_types.json | 346 +++++ .../schemas/v0_10/server_to_client.json | 257 +++ .../schemas/v0_9/basic_catalog.json | 1383 +++++++++++++++++ .../schemas/v0_9/basic_catalog_rules.txt | 5 + .../a2uischema/schemas/v0_9/common_types.json | 305 ++++ .../schemas/v0_9/server_to_client.json | 132 ++ .../schemas/v0_9_1/basic_catalog.json | 1383 +++++++++++++++++ .../schemas/v0_9_1/basic_catalog_rules.txt | 5 + .../schemas/v0_9_1/common_types.json | 305 ++++ .../schemas/v0_9_1/server_to_client.json | 132 ++ .../basic/examples/01_flight-status.json | 201 +++ .../basic/examples/02_email-compose.json | 185 +++ .../v0_10/basic/examples/03_calendar-day.json | 166 ++ .../basic/examples/04_weather-current.json | 168 ++ .../v0_10/basic/examples/05_product-card.json | 151 ++ .../v0_10/basic/examples/06_music-player.json | 165 ++ .../v0_10/basic/examples/07_task-card.json | 107 ++ .../v0_10/basic/examples/08_user-profile.json | 190 +++ .../v0_10/basic/examples/09_login-form.json | 214 +++ .../examples/10_notification-permission.json | 105 ++ .../basic/examples/11_purchase-complete.json | 169 ++ .../v0_10/basic/examples/12_chat-message.json | 144 ++ .../v0_10/basic/examples/13_coffee-order.json | 253 +++ .../basic/examples/14_sports-player.json | 177 +++ .../basic/examples/15_account-balance.json | 126 ++ .../basic/examples/16_workout-summary.json | 160 ++ .../v0_10/basic/examples/17_event-detail.json | 144 ++ .../v0_10/basic/examples/18_track-list.json | 152 ++ .../basic/examples/19_software-purchase.json | 194 +++ .../basic/examples/20_restaurant-card.json | 140 ++ .../basic/examples/21_shipping-status.json | 137 ++ .../v0_10/basic/examples/22_credit-card.json | 117 ++ .../v0_10/basic/examples/23_step-counter.json | 149 ++ .../v0_10/basic/examples/24_recipe-card.json | 204 +++ .../v0_10/basic/examples/25_contact-card.json | 175 +++ .../basic/examples/26_podcast-episode.json | 123 ++ .../v0_10/basic/examples/27_stats-card.json | 106 ++ .../basic/examples/28_countdown-timer.json | 135 ++ .../v0_10/basic/examples/29_movie-card.json | 156 ++ .../examples/30_live-invitation-builder.json | 205 +++ .../examples/31_incremental-dashboard.json | 128 ++ .../examples/32_advanced-form-validator.json | 166 ++ .../examples/33_financial-data-grid.json | 171 ++ .../examples/34_child-list-template.json | 80 + .../basic/examples/35_markdown-text.json | 44 + .../v0_10/basic/examples/36_modal.json | 65 + agent_sdks/go/a2uischema/validator.go | 687 ++++++++ agent_sdks/go/a2uischema/validator_v010.go | 619 ++++++++ agent_sdks/go/a2uistream/doc.go | 3 + agent_sdks/go/a2uistream/example_test.go | 82 + agent_sdks/go/a2uistream/parser.go | 560 +++++++ agent_sdks/go/a2uistream/parser_test.go | 517 ++++++ agent_sdks/go/a2uistream/payload.go | 148 ++ .../go/a2uistream/payload_conformance_test.go | 175 +++ agent_sdks/go/a2uistream/v08.go | 350 +++++ agent_sdks/go/a2uistream/v08_test.go | 91 ++ agent_sdks/go/a2uistream/validate.go | 33 + agent_sdks/go/cmd/a2uigen/main_test.go | 44 + 258 files changed, 42117 insertions(+) create mode 100644 agent_sdks/go/a2a/a2a.go create mode 100644 agent_sdks/go/a2a/a2a_test.go create mode 100644 agent_sdks/go/a2a/conformance_test.go create mode 100644 agent_sdks/go/a2a/doc.go create mode 100644 agent_sdks/go/a2a/example_test.go create mode 100644 agent_sdks/go/a2ui/a2ui.go create mode 100644 agent_sdks/go/a2ui/doc.go create mode 100644 agent_sdks/go/a2ui/example_test.go create mode 100644 agent_sdks/go/a2ui/v010/capabilities.go create mode 100644 agent_sdks/go/a2ui/v010/common.go create mode 100644 agent_sdks/go/a2ui/v010/common_json.go create mode 100644 agent_sdks/go/a2ui/v010/common_test.go create mode 100644 agent_sdks/go/a2ui/v010/component.go create mode 100644 agent_sdks/go/a2ui/v010/component_test.go create mode 100644 agent_sdks/go/a2ui/v010/doc.go create mode 100644 agent_sdks/go/a2ui/v010/dynamic.go create mode 100644 agent_sdks/go/a2ui/v010/dynamic_json.go create mode 100644 agent_sdks/go/a2ui/v010/dynamic_test.go create mode 100644 agent_sdks/go/a2ui/v010/example_test.go create mode 100644 agent_sdks/go/a2ui/v010/gen.go create mode 100644 agent_sdks/go/a2ui/v010/message.go create mode 100644 agent_sdks/go/a2ui/v010/message_json.go create mode 100644 agent_sdks/go/a2ui/v010/message_test.go create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/01_flight-status.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/02_email-compose.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/03_calendar-day.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/04_weather-current.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/05_product-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/06_music-player.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/07_task-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/08_user-profile.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/09_login-form.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/10_notification-permission.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/11_purchase-complete.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/12_chat-message.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/13_coffee-order.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/14_sports-player.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/15_account-balance.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/16_workout-summary.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/17_event-detail.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/18_track-list.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/19_software-purchase.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/20_restaurant-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/21_shipping-status.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/22_credit-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/23_step-counter.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/24_recipe-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/25_contact-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/26_podcast-episode.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/27_stats-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/28_countdown-timer.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/29_movie-card.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/30_live-invitation-builder.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/31_incremental-dashboard.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/32_advanced-form-validator.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/33_financial-data-grid.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/34_child-list-template.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/35_markdown-text.json create mode 100644 agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/36_modal.json create mode 100644 agent_sdks/go/a2ui/v010/zz_component.go create mode 100644 agent_sdks/go/a2ui/v010/zz_component_marshal.go create mode 100644 agent_sdks/go/a2ui/v010/zz_enum.go create mode 100644 agent_sdks/go/a2ui/v010/zz_function.go create mode 100644 agent_sdks/go/a2ui/v010/zz_icon.go create mode 100644 agent_sdks/go/a2ui/v010/zz_wrapper.go create mode 100644 agent_sdks/go/a2ui/v09/capabilities.go create mode 100644 agent_sdks/go/a2ui/v09/common.go create mode 100644 agent_sdks/go/a2ui/v09/common_json.go create mode 100644 agent_sdks/go/a2ui/v09/common_test.go create mode 100644 agent_sdks/go/a2ui/v09/component.go create mode 100644 agent_sdks/go/a2ui/v09/component_test.go create mode 100644 agent_sdks/go/a2ui/v09/doc.go create mode 100644 agent_sdks/go/a2ui/v09/dynamic.go create mode 100644 agent_sdks/go/a2ui/v09/dynamic_json.go create mode 100644 agent_sdks/go/a2ui/v09/dynamic_test.go create mode 100644 agent_sdks/go/a2ui/v09/example_test.go create mode 100644 agent_sdks/go/a2ui/v09/gen.go create mode 100644 agent_sdks/go/a2ui/v09/message.go create mode 100644 agent_sdks/go/a2ui/v09/message_json.go create mode 100644 agent_sdks/go/a2ui/v09/message_test.go create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/01_flight-status.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/02_email-compose.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/03_calendar-day.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/04_weather-current.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/05_product-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/06_music-player.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/07_task-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/08_user-profile.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/09_login-form.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/10_notification-permission.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/11_purchase-complete.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/12_chat-message.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/13_coffee-order.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/14_sports-player.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/15_account-balance.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/16_workout-summary.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/17_event-detail.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/18_track-list.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/19_software-purchase.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/20_restaurant-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/21_shipping-status.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/22_credit-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/23_step-counter.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/24_recipe-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/25_contact-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/26_podcast-episode.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/27_stats-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/28_countdown-timer.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/29_movie-card.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/30_live-invitation-builder.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/31_incremental-dashboard.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/32_advanced-form-validator.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/33_financial-data-grid.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/34_child-list-template.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/35_markdown-text.json create mode 100644 agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/36_modal.json create mode 100644 agent_sdks/go/a2ui/v09/zz_component.go create mode 100644 agent_sdks/go/a2ui/v09/zz_component_marshal.go create mode 100644 agent_sdks/go/a2ui/v09/zz_enum.go create mode 100644 agent_sdks/go/a2ui/v09/zz_function.go create mode 100644 agent_sdks/go/a2ui/v09/zz_icon.go create mode 100644 agent_sdks/go/a2ui/v09/zz_wrapper.go create mode 100644 agent_sdks/go/a2ui/v091/capabilities.go create mode 100644 agent_sdks/go/a2ui/v091/common.go create mode 100644 agent_sdks/go/a2ui/v091/common_json.go create mode 100644 agent_sdks/go/a2ui/v091/common_test.go create mode 100644 agent_sdks/go/a2ui/v091/component.go create mode 100644 agent_sdks/go/a2ui/v091/component_test.go create mode 100644 agent_sdks/go/a2ui/v091/doc.go create mode 100644 agent_sdks/go/a2ui/v091/dynamic.go create mode 100644 agent_sdks/go/a2ui/v091/dynamic_json.go create mode 100644 agent_sdks/go/a2ui/v091/dynamic_test.go create mode 100644 agent_sdks/go/a2ui/v091/example_test.go create mode 100644 agent_sdks/go/a2ui/v091/gen.go create mode 100644 agent_sdks/go/a2ui/v091/message.go create mode 100644 agent_sdks/go/a2ui/v091/message_json.go create mode 100644 agent_sdks/go/a2ui/v091/message_test.go create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/01_flight-status.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/02_email-compose.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/03_calendar-day.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/04_weather-current.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/05_product-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/06_music-player.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/07_task-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/08_user-profile.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/09_login-form.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/10_notification-permission.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/11_purchase-complete.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/12_chat-message.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/13_coffee-order.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/14_sports-player.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/15_account-balance.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/16_workout-summary.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/17_event-detail.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/18_track-list.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/19_software-purchase.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/20_restaurant-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/21_shipping-status.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/22_credit-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/23_step-counter.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/24_recipe-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/25_contact-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/26_podcast-episode.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/27_stats-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/28_countdown-timer.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/29_movie-card.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/30_live-invitation-builder.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/31_incremental-dashboard.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/32_advanced-form-validator.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/33_financial-data-grid.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/34_child-list-template.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/35_markdown-text.json create mode 100644 agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/36_modal.json create mode 100644 agent_sdks/go/a2ui/v091/zz_component.go create mode 100644 agent_sdks/go/a2ui/v091/zz_component_marshal.go create mode 100644 agent_sdks/go/a2ui/v091/zz_enum.go create mode 100644 agent_sdks/go/a2ui/v091/zz_function.go create mode 100644 agent_sdks/go/a2ui/v091/zz_icon.go create mode 100644 agent_sdks/go/a2ui/v091/zz_wrapper.go create mode 100644 agent_sdks/go/a2uiadk/doc.go create mode 100644 agent_sdks/go/a2uiadk/example_test.go create mode 100644 agent_sdks/go/a2uiadk/tool.go create mode 100644 agent_sdks/go/a2uiadk/tool_test.go create mode 100644 agent_sdks/go/a2uibuild/children.go create mode 100644 agent_sdks/go/a2uibuild/doc.go create mode 100644 agent_sdks/go/a2uibuild/example_test.go create mode 100644 agent_sdks/go/a2uibuild/surface.go create mode 100644 agent_sdks/go/a2uibuild/surface_test.go create mode 100644 agent_sdks/go/a2uibuild/zz_builders.go create mode 100644 agent_sdks/go/a2uischema/assets.go create mode 100644 agent_sdks/go/a2uischema/catalog.go create mode 100644 agent_sdks/go/a2uischema/catalog_conformance_test.go create mode 100644 agent_sdks/go/a2uischema/constants.go create mode 100644 agent_sdks/go/a2uischema/doc.go create mode 100644 agent_sdks/go/a2uischema/error.go create mode 100644 agent_sdks/go/a2uischema/manager.go create mode 100644 agent_sdks/go/a2uischema/provider.go create mode 100644 agent_sdks/go/a2uischema/schema_test.go create mode 100644 agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog_rules.txt create mode 100644 agent_sdks/go/a2uischema/schemas/v0_10/common_types.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_10/server_to_client.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog_rules.txt create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9/common_types.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9/server_to_client.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog_rules.txt create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9_1/common_types.json create mode 100644 agent_sdks/go/a2uischema/schemas/v0_9_1/server_to_client.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/01_flight-status.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/02_email-compose.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/03_calendar-day.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/04_weather-current.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/05_product-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/06_music-player.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/07_task-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/08_user-profile.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/09_login-form.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/10_notification-permission.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/11_purchase-complete.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/12_chat-message.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/13_coffee-order.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/14_sports-player.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/15_account-balance.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/16_workout-summary.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/17_event-detail.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/18_track-list.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/19_software-purchase.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/20_restaurant-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/21_shipping-status.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/22_credit-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/23_step-counter.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/24_recipe-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/25_contact-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/26_podcast-episode.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/27_stats-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/28_countdown-timer.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/29_movie-card.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/30_live-invitation-builder.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/31_incremental-dashboard.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/32_advanced-form-validator.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/33_financial-data-grid.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/34_child-list-template.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/35_markdown-text.json create mode 100644 agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/36_modal.json create mode 100644 agent_sdks/go/a2uischema/validator.go create mode 100644 agent_sdks/go/a2uischema/validator_v010.go create mode 100644 agent_sdks/go/a2uistream/doc.go create mode 100644 agent_sdks/go/a2uistream/example_test.go create mode 100644 agent_sdks/go/a2uistream/parser.go create mode 100644 agent_sdks/go/a2uistream/parser_test.go create mode 100644 agent_sdks/go/a2uistream/payload.go create mode 100644 agent_sdks/go/a2uistream/payload_conformance_test.go create mode 100644 agent_sdks/go/a2uistream/v08.go create mode 100644 agent_sdks/go/a2uistream/v08_test.go create mode 100644 agent_sdks/go/a2uistream/validate.go diff --git a/agent_sdks/go/a2a/a2a.go b/agent_sdks/go/a2a/a2a.go new file mode 100644 index 0000000000..195f055a99 --- /dev/null +++ b/agent_sdks/go/a2a/a2a.go @@ -0,0 +1,271 @@ +package a2a + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + "strconv" + "strings" +) + +const ( + A2UIExtensionBaseURI = "https://a2ui.org/a2a-extension/a2ui" + MIMETypeKey = "mimeType" + A2UIMIMETypeV09 = "application/json+a2ui" + A2UIMIMETypeV091 = "application/a2ui+json" + A2UIMIMETypeV010 = "application/a2ui+json" + MIMETypeV09 = A2UIMIMETypeV09 + MIMETypeV091 = A2UIMIMETypeV091 + MIMETypeV010 = A2UIMIMETypeV010 + A2UIMIMEType = A2UIMIMETypeV09 + A2UIMIMETypeLatest = A2UIMIMETypeV010 + MIMEType = A2UIMIMEType + MIMETypeLatest = A2UIMIMETypeLatest + AcceptsInlineCatalogsKey = "acceptsInlineCatalogs" + SupportedCatalogIDsKey = "supportedCatalogIds" +) + +// DataPart is a transport-neutral A2A data part carrying A2UI JSON. +// Its shape matches the official A2A Go SDK's DataPart. +type DataPart struct { + Data map[string]any `json:"data"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Part is kept as a compatibility alias for earlier versions of this package. +type Part = DataPart + +// AgentExtension is a transport-neutral A2A agent extension descriptor. +// Its shape matches the official A2A Go SDK's AgentExtension. +type AgentExtension struct { + Description string `json:"description,omitempty"` + Params map[string]any `json:"params,omitempty"` + Required bool `json:"required,omitempty"` + URI string `json:"uri"` +} + +// Extension is kept as a compatibility alias for earlier versions of this package. +type Extension = AgentExtension + +// Versioned reports the A2UI protocol version carried by a payload. +type Versioned interface { + VersionString() string +} + +// Meta returns the part metadata. +func (p DataPart) Meta() map[string]any { + return p.Metadata +} + +// SetMeta sets a metadata entry. +func (p *DataPart) SetMeta(k string, v any) { + if p.Metadata == nil { + p.Metadata = make(map[string]any) + } + p.Metadata[k] = v +} + +// MarshalA2UIData marshals payload into an A2A data-part payload. +// A2A data parts carry JSON objects, so payload must encode as a JSON object. +func MarshalA2UIData(payload any) (map[string]any, error) { + if object, ok := payload.(map[string]any); ok { + if object == nil { + return nil, fmt.Errorf("a2a: payload must encode as a JSON object") + } + return maps.Clone(object), nil + } + data, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("a2a: marshal payload: %w", err) + } + var object map[string]any + if err := json.Unmarshal(data, &object); err != nil { + return nil, fmt.Errorf("a2a: decode payload object: %w", err) + } + if object == nil { + return nil, fmt.Errorf("a2a: payload must encode as a JSON object") + } + return object, nil +} + +// CreateDataPart marshals an A2UI payload into a transport-neutral A2A data part. +func CreateDataPart(payload any) (DataPart, error) { + return CreateDataPartForVersion(payload, "") +} + +// CreateDataPartForVersion marshals an A2UI payload using the MIME type for version. +func CreateDataPartForVersion(payload any, version string) (DataPart, error) { + if version == "" { + if versioned, ok := payload.(Versioned); ok { + version = versioned.VersionString() + } + } + data, err := MarshalA2UIData(payload) + if err != nil { + return DataPart{}, err + } + if version == "" { + version, _ = data["version"].(string) + } + part := DataPart{Data: data} + if version == "" { + part.SetMeta(MIMETypeKey, A2UIMIMETypeLatest) + } else { + part.SetMeta(MIMETypeKey, MIMETypeForVersion(version)) + } + return part, nil +} + +// CreatePart marshals an A2UI payload into a transport-neutral A2A data part. +func CreatePart(payload any) (Part, error) { + return CreateDataPart(payload) +} + +// IsA2UIPart reports whether the part carries the A2UI MIME type. +func IsA2UIPart(part DataPart) bool { + return IsPart(part) +} + +// IsPart reports whether the part carries the A2UI MIME type. +func IsPart(part DataPart) bool { + if part.Metadata == nil { + return false + } + mimeType, _ := part.Metadata[MIMETypeKey].(string) + return IsA2UIMIMEType(mimeType) +} + +// IsA2UIMIMEType reports whether mimeType is a recognized A2UI MIME type. +func IsA2UIMIMEType(mimeType string) bool { + return mimeType == A2UIMIMETypeV09 || mimeType == A2UIMIMETypeV091 || mimeType == A2UIMIMETypeV010 +} + +// MIMETypeForVersion returns the A2A MIME type used by an A2UI version. +func MIMETypeForVersion(version string) string { + switch normalizeVersion(version) { + case "v0.9": + return A2UIMIMETypeV09 + case "v0.9.1": + return A2UIMIMETypeV091 + default: + return A2UIMIMETypeLatest + } +} + +// A2UIData returns the structured A2UI payload if the part carries A2UI data. +func A2UIData(part DataPart) (map[string]any, bool) { + return Data(part) +} + +// Data returns the structured A2UI payload if the part carries A2UI data. +func Data(part DataPart) (map[string]any, bool) { + if !IsPart(part) { + return nil, false + } + return part.Data, true +} + +// AgentExtensionOptions configures an A2A agent extension descriptor. +type AgentExtensionOptions struct { + Version string + AcceptsInlineCatalogs bool + SupportedCatalogIDs []string +} + +// NewAgentExtension constructs an A2UI extension descriptor. +func NewAgentExtension(opts AgentExtensionOptions) AgentExtension { + params := make(map[string]any) + if opts.AcceptsInlineCatalogs { + params[AcceptsInlineCatalogsKey] = true + } + if len(opts.SupportedCatalogIDs) > 0 { + params[SupportedCatalogIDsKey] = append([]string(nil), opts.SupportedCatalogIDs...) + } + if len(params) == 0 { + params = nil + } + return AgentExtension{ + URI: fmt.Sprintf("%s/%s", A2UIExtensionBaseURI, normalizeVersion(opts.Version)), + Description: "Provides agent driven UI using the A2UI JSON format.", + Params: params, + } +} + +// NewExtension constructs an A2UI extension descriptor. +func NewExtension(opts AgentExtensionOptions) Extension { + return NewAgentExtension(opts) +} + +// SelectNewestRequestedExtension returns the newest requested extension also advertised by the agent. +func SelectNewestRequestedExtension(requested, advertised []string) (string, bool) { + best := "" + for _, candidate := range requested { + if !slices.Contains(advertised, candidate) { + continue + } + if best == "" || compareExtensionVersion(candidate, best) > 0 { + best = candidate + } + } + if best == "" { + return "", false + } + return best, true +} + +// TryActivateExtension selects and activates the newest mutually supported extension. +func TryActivateExtension(requested, advertised []string) (activated, version string, ok bool) { + activated, ok = SelectNewestRequestedExtension(requested, advertised) + if !ok { + return "", "", false + } + version = strings.TrimPrefix(activated, A2UIExtensionBaseURI+"/") + version = strings.TrimPrefix(version, "v") + return activated, version, true +} + +func normalizeVersion(version string) string { + version = strings.TrimSpace(version) + version = strings.TrimPrefix(version, "v") + if version == "" { + return "v0.9" + } + return "v" + version +} + +func compareExtensionVersion(a, b string) int { + av := strings.TrimPrefix(strings.TrimPrefix(a, A2UIExtensionBaseURI+"/"), "v") + bv := strings.TrimPrefix(strings.TrimPrefix(b, A2UIExtensionBaseURI+"/"), "v") + aparts := parseVersionParts(av) + bparts := parseVersionParts(bv) + for i := 0; i < len(aparts) || i < len(bparts); i++ { + var ai, bi int + if i < len(aparts) { + ai = aparts[i] + } + if i < len(bparts) { + bi = bparts[i] + } + switch { + case ai < bi: + return -1 + case ai > bi: + return 1 + } + } + return 0 +} + +func parseVersionParts(version string) []int { + fields := strings.Split(version, ".") + out := make([]int, 0, len(fields)) + for _, field := range fields { + n, err := strconv.Atoi(field) + if err != nil { + return []int{0} + } + out = append(out, n) + } + return out +} diff --git a/agent_sdks/go/a2a/a2a_test.go b/agent_sdks/go/a2a/a2a_test.go new file mode 100644 index 0000000000..3fa786f0f5 --- /dev/null +++ b/agent_sdks/go/a2a/a2a_test.go @@ -0,0 +1,116 @@ +package a2a + +import "testing" + +func TestCreatePart(t *testing.T) { + part, err := CreateDataPart(map[string]any{"version": "v0.9"}) + if err != nil { + t.Fatal(err) + } + if !IsPart(part) { + t.Fatal("expected A2UI part") + } + if _, ok := Data(part); !ok { + t.Fatal("expected A2UI data") + } + if got := part.Metadata[MIMETypeKey]; got != A2UIMIMETypeV09 { + t.Fatalf("mime type = %q, want %q", got, A2UIMIMETypeV09) + } +} + +func TestCreatePartUsesVersionedMIMEType(t *testing.T) { + tests := []struct { + name string + version string + want string + }{ + {"v0.9", "v0.9", A2UIMIMETypeV09}, + {"v0.9.1", "v0.9.1", A2UIMIMETypeV091}, + {"v0.10", "v0.10", A2UIMIMETypeV010}, + {"default", "", A2UIMIMETypeLatest}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + part, err := CreateDataPartForVersion(map[string]any{"version": tt.version}, tt.version) + if err != nil { + t.Fatal(err) + } + if got := part.Metadata[MIMETypeKey]; got != tt.want { + t.Fatalf("mime type = %q, want %q", got, tt.want) + } + if !IsPart(part) { + t.Fatal("expected A2UI part") + } + }) + } +} + +func TestCreateDataPartInfersVersionedPayload(t *testing.T) { + part, err := CreateDataPart(versionedPayload{Version: "v0.10", Kind: "demo"}) + if err != nil { + t.Fatal(err) + } + if got := part.Metadata[MIMETypeKey]; got != A2UIMIMETypeV010 { + t.Fatalf("mime type = %q, want %q", got, A2UIMIMETypeV010) + } +} + +func TestMarshalA2UIDataClonesMapPayload(t *testing.T) { + payload := map[string]any{"version": "v0.10"} + data, err := MarshalA2UIData(payload) + if err != nil { + t.Fatal(err) + } + data["version"] = "changed" + if got := payload["version"]; got != "v0.10" { + t.Fatalf("payload version = %q, want unchanged", got) + } +} + +func TestCreateDataPartRejectsNonObject(t *testing.T) { + if _, err := CreateDataPart([]string{"not", "an", "object"}); err == nil { + t.Fatal("expected error") + } +} + +func TestNewAgentExtension(t *testing.T) { + ext := NewAgentExtension(AgentExtensionOptions{ + Version: "0.9", + AcceptsInlineCatalogs: true, + SupportedCatalogIDs: []string{"catalog"}, + }) + if ext.URI != "https://a2ui.org/a2a-extension/a2ui/v0.9" { + t.Fatalf("uri = %q", ext.URI) + } + if ext.Params[AcceptsInlineCatalogsKey] != true { + t.Fatal("expected acceptsInlineCatalogs param") + } +} + +func TestSelectNewestRequestedExtension(t *testing.T) { + got, ok := SelectNewestRequestedExtension( + []string{ + "https://a2ui.org/a2a-extension/a2ui/v0.8", + "https://a2ui.org/a2a-extension/a2ui/v0.9", + }, + []string{ + "https://a2ui.org/a2a-extension/a2ui/v0.8", + "https://a2ui.org/a2a-extension/a2ui/v0.9", + }, + ) + if !ok { + t.Fatal("expected a match") + } + if want := "https://a2ui.org/a2a-extension/a2ui/v0.9"; got != want { + t.Fatalf("got %q, want %q", got, want) + } +} + +type versionedPayload struct { + Version string `json:"version"` + Kind string `json:"kind"` +} + +func (p versionedPayload) VersionString() string { + return p.Version +} diff --git a/agent_sdks/go/a2a/conformance_test.go b/agent_sdks/go/a2a/conformance_test.go new file mode 100644 index 0000000000..4067d2de4a --- /dev/null +++ b/agent_sdks/go/a2a/conformance_test.go @@ -0,0 +1,219 @@ +package a2a + +import ( + "reflect" + "testing" +) + +type conformanceCase struct { + Name string + Action string + Args map[string]any + Expect any +} + +func TestA2AConformance(t *testing.T) { + for _, tc := range a2aConformanceCases() { + t.Run(tc.Name, func(t *testing.T) { + runA2AConformanceCase(t, tc) + }) + } +} + +func runA2AConformanceCase(t *testing.T, tc conformanceCase) { + switch tc.Action { + case "create_a2ui_part": + part, err := CreateDataPart(tc.Args["data"]) + if err != nil { + t.Fatal(err) + } + if !IsPart(part) { + t.Fatal("part is not A2UI") + } + expect := expectMap(t, tc.Expect) + if got, want := part.Metadata[MIMETypeKey], expect["mime_type"]; got != want { + t.Fatalf("mime type = %q, want %q", got, want) + } + case "is_a2ui_part": + part := DataPart{Data: map[string]any{}, Metadata: map[string]any{MIMETypeKey: tc.Args["mime_type"]}} + if got, want := IsPart(part), tc.Expect; got != want { + t.Fatalf("IsPart = %v, want %v", got, want) + } + case "get_extension": + opts := AgentExtensionOptions{ + Version: tc.Args["version"].(string), + } + if v, ok := tc.Args["accepts_inline_catalogs"].(bool); ok { + opts.AcceptsInlineCatalogs = v + } + if v, ok := tc.Args["supported_catalog_ids"].([]any); ok { + opts.SupportedCatalogIDs = stringSlice(v) + } + ext := NewAgentExtension(opts) + expect := expectMap(t, tc.Expect) + if got, want := ext.URI, expect["uri"]; got != want { + t.Fatalf("uri = %q, want %q", got, want) + } + if !reflect.DeepEqual(normalizeNilMap(ext.Params), normalizeMap(expect["params"])) { + t.Fatalf("params = %#v, want %#v", ext.Params, expect["params"]) + } + case "try_activate": + activated, version, ok := TryActivateExtension(stringSlice(tc.Args["requested"].([]any)), stringSlice(tc.Args["advertised"].([]any))) + expect := expectMap(t, tc.Expect) + if expect["activated"] == nil { + if ok { + t.Fatalf("activated = %q, version = %q, want no activation", activated, version) + } + return + } + if !ok { + t.Fatal("activation failed") + } + if got, want := activated, expect["activated"]; got != want { + t.Fatalf("activated = %q, want %q", got, want) + } + if got, want := version, expect["version"]; got != want { + t.Fatalf("version = %q, want %q", got, want) + } + case "select_newest": + got, _ := SelectNewestRequestedExtension(stringSlice(tc.Args["requested"].([]any)), stringSlice(tc.Args["advertised"].([]any))) + expect := expectMap(t, tc.Expect) + if want := expect["newest"]; got != want { + t.Fatalf("newest = %q, want %q", got, want) + } + default: + t.Fatalf("unsupported action %q", tc.Action) + } +} + +func expectMap(t *testing.T, v any) map[string]any { + t.Helper() + m, ok := v.(map[string]any) + if !ok { + t.Fatalf("expect has type %T, want map", v) + } + return m +} + +func stringSlice(values []any) []string { + out := make([]string, 0, len(values)) + for _, value := range values { + out = append(out, value.(string)) + } + return out +} + +func normalizeMap(v any) any { + switch v := v.(type) { + case map[string]any: + out := make(map[string]any, len(v)) + for key, value := range v { + out[key] = normalizeMap(value) + } + return out + case []any: + return stringSlice(v) + default: + return v + } +} + +func normalizeNilMap(m map[string]any) any { + if m == nil { + return nil + } + return m +} + +func a2aConformanceCases() []conformanceCase { + // These cases mirror agent_sdks/conformance/suites/a2a_integration.yaml. + return []conformanceCase{ + { + Name: "test_create_a2ui_part", + Action: "create_a2ui_part", + Args: map[string]any{ + "data": map[string]any{"foo": "bar"}, + }, + Expect: map[string]any{"mime_type": "application/a2ui+json"}, + }, + { + Name: "test_is_a2ui_part", + Action: "is_a2ui_part", + Args: map[string]any{"mime_type": "application/a2ui+json"}, + Expect: true, + }, + { + Name: "test_get_extension_minimal", + Action: "get_extension", + Args: map[string]any{"version": "0.8"}, + Expect: map[string]any{ + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8", + "params": nil, + }, + }, + { + Name: "test_get_extension_with_inline", + Action: "get_extension", + Args: map[string]any{ + "version": "0.8", + "accepts_inline_catalogs": true, + }, + Expect: map[string]any{ + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8", + "params": map[string]any{"acceptsInlineCatalogs": true}, + }, + }, + { + Name: "test_get_extension_with_catalogs", + Action: "get_extension", + Args: map[string]any{ + "version": "0.8", + "supported_catalog_ids": []any{"a", "b", "c"}, + }, + Expect: map[string]any{ + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8", + "params": map[string]any{"supportedCatalogIds": []any{"a", "b", "c"}}, + }, + }, + { + Name: "test_try_activate_success", + Action: "try_activate", + Args: map[string]any{ + "requested": []any{"https://a2ui.org/a2a-extension/a2ui/v0.8"}, + "advertised": []any{"https://a2ui.org/a2a-extension/a2ui/v0.8"}, + }, + Expect: map[string]any{ + "activated": "https://a2ui.org/a2a-extension/a2ui/v0.8", + "version": "0.8", + }, + }, + { + Name: "test_try_activate_not_requested", + Action: "try_activate", + Args: map[string]any{ + "requested": []any{}, + "advertised": []any{"https://a2ui.org/a2a-extension/a2ui/v0.8"}, + }, + Expect: map[string]any{"activated": nil}, + }, + { + Name: "test_select_newest", + Action: "select_newest", + Args: map[string]any{ + "requested": []any{ + "https://a2ui.org/a2a-extension/a2ui/v0.1.0", + "https://a2ui.org/a2a-extension/a2ui/v1.2.0", + "https://a2ui.org/a2a-extension/a2ui/v0.8.0", + "https://a2ui.org/a2a-extension/a2ui/v1.10.0", + }, + "advertised": []any{ + "https://a2ui.org/a2a-extension/a2ui/v0.1.0", + "https://a2ui.org/a2a-extension/a2ui/v1.2.0", + "https://a2ui.org/a2a-extension/a2ui/v1.10.0", + "https://a2ui.org/a2a-extension/a2ui/v2.0.0", + }, + }, + Expect: map[string]any{"newest": "https://a2ui.org/a2a-extension/a2ui/v1.10.0"}, + }, + } +} diff --git a/agent_sdks/go/a2a/doc.go b/agent_sdks/go/a2a/doc.go new file mode 100644 index 0000000000..4efa828be6 --- /dev/null +++ b/agent_sdks/go/a2a/doc.go @@ -0,0 +1,7 @@ +// Package a2a provides lightweight A2A helper types for transporting A2UI +// payloads without depending on a specific A2A Go implementation. +// +// The exported types mirror the shape of the official A2A Go SDK's data part +// and agent extension types, so code can adopt github.com/a2aproject/a2a-go/v2 +// later with minimal API churn. +package a2a diff --git a/agent_sdks/go/a2a/example_test.go b/agent_sdks/go/a2a/example_test.go new file mode 100644 index 0000000000..bd075d489c --- /dev/null +++ b/agent_sdks/go/a2a/example_test.go @@ -0,0 +1,39 @@ +package a2a_test + +import ( + "fmt" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2a" +) + +func ExampleCreateDataPart() { + part, err := a2a.CreateDataPart(map[string]any{ + "version": "v0.10", + "updateDataModel": map[string]any{ + "surfaceId": "dashboard", + "value": map[string]any{"status": "ready"}, + }, + }) + if err != nil { + panic(err) + } + fmt.Println(part.Metadata[a2a.MIMETypeKey]) + fmt.Println(a2a.IsPart(part)) + // Output: + // application/a2ui+json + // true +} + +func ExampleTryActivateExtension() { + activated, version, ok := a2a.TryActivateExtension( + []string{"https://a2ui.org/a2a-extension/a2ui/v0.9"}, + []string{"https://a2ui.org/a2a-extension/a2ui/v0.9"}, + ) + fmt.Println(activated) + fmt.Println(version) + fmt.Println(ok) + // Output: + // https://a2ui.org/a2a-extension/a2ui/v0.9 + // 0.9 + // true +} diff --git a/agent_sdks/go/a2ui/a2ui.go b/agent_sdks/go/a2ui/a2ui.go new file mode 100644 index 0000000000..cce848c64e --- /dev/null +++ b/agent_sdks/go/a2ui/a2ui.go @@ -0,0 +1,272 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package a2ui + +import "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v09" + +// Version re-exports the protocol version from the active implementation. +const Version = v09.Version + +// Hand-written message types. +type ( + ServerMessage = v09.ServerMessage + ClientMessage = v09.ClientMessage +) + +// Hand-written server-to-client message types. +type ( + CreateSurface = v09.CreateSurface + UpdateComponents = v09.UpdateComponents + UpdateDataModel = v09.UpdateDataModel + DeleteSurface = v09.DeleteSurface +) + +// Hand-written client-to-server message types. +type ( + ActionEvent = v09.ActionEvent + ClientError = v09.ClientError +) + +// Hand-written common types. +type ( + Component = v09.Component + DynamicString = v09.DynamicString + DynamicNumber = v09.DynamicNumber + DynamicBoolean = v09.DynamicBoolean + DynamicStringList = v09.DynamicStringList + DynamicValue = v09.DynamicValue + DataBinding = v09.DataBinding + FunctionCall = v09.FunctionCall + ChildList = v09.ChildList + ChildTemplate = v09.ChildTemplate + CheckRule = v09.CheckRule + AccessibilityAttributes = v09.AccessibilityAttributes + Theme = v09.Theme + Action = v09.Action + EventAction = v09.EventAction + IconNameOrPath = v09.IconNameOrPath +) + +// Capability types. +type ( + ClientCapabilities = v09.ClientCapabilities + ClientCapabilitiesV09 = v09.ClientCapabilitiesV09 + ServerCapabilities = v09.ServerCapabilities + ServerCapabilitiesV09 = v09.ServerCapabilitiesV09 + CatalogDef = v09.CatalogDef + FunctionDefinition = v09.FunctionDefinition + ClientDataModel = v09.ClientDataModel +) + +// Generated component types. +type ( + AudioPlayerComponent = v09.AudioPlayerComponent + ButtonComponent = v09.ButtonComponent + CardComponent = v09.CardComponent + CheckBoxComponent = v09.CheckBoxComponent + ChoicePickerComponent = v09.ChoicePickerComponent + ColumnComponent = v09.ColumnComponent + DateTimeInputComponent = v09.DateTimeInputComponent + DividerComponent = v09.DividerComponent + IconComponent = v09.IconComponent + ImageComponent = v09.ImageComponent + ListComponent = v09.ListComponent + ModalComponent = v09.ModalComponent + RowComponent = v09.RowComponent + SliderComponent = v09.SliderComponent + TabsComponent = v09.TabsComponent + TextComponent = v09.TextComponent + TextFieldComponent = v09.TextFieldComponent + VideoComponent = v09.VideoComponent +) + +// Generated inline struct types. +type ( + TabDef = v09.TabDef + ChoiceOption = v09.ChoiceOption +) + +// Generated message list-wrapper types. +type ( + ClientMessageListWrapper = v09.ClientMessageListWrapper + ServerMessageListWrapper = v09.ServerMessageListWrapper +) + +// Generated enum types. +type ( + ReturnType = v09.ReturnType + IconName = v09.IconName + ButtonVariant = v09.ButtonVariant + ChoicePickerDisplayStyle = v09.ChoicePickerDisplayStyle + ChoicePickerVariant = v09.ChoicePickerVariant + DividerAxis = v09.DividerAxis + ImageFit = v09.ImageFit + ImageVariant = v09.ImageVariant + LayoutAlign = v09.LayoutAlign + LayoutJustify = v09.LayoutJustify + ListDirection = v09.ListDirection + TextFieldVariant = v09.TextFieldVariant + TextVariant = v09.TextVariant +) + +// ReturnType constants. +const ( + ReturnTypeString = v09.ReturnTypeString + ReturnTypeNumber = v09.ReturnTypeNumber + ReturnTypeBoolean = v09.ReturnTypeBoolean + ReturnTypeArray = v09.ReturnTypeArray + ReturnTypeObject = v09.ReturnTypeObject + ReturnTypeAny = v09.ReturnTypeAny + ReturnTypeVoid = v09.ReturnTypeVoid +) + +// Icon constants. +const ( + IconAccountCircle = v09.IconAccountCircle + IconAdd = v09.IconAdd + IconArrowBack = v09.IconArrowBack + IconArrowForward = v09.IconArrowForward + IconAttachFile = v09.IconAttachFile + IconCalendarToday = v09.IconCalendarToday + IconCall = v09.IconCall + IconCamera = v09.IconCamera + IconCheck = v09.IconCheck + IconClose = v09.IconClose + IconDelete = v09.IconDelete + IconDownload = v09.IconDownload + IconEdit = v09.IconEdit + IconEvent = v09.IconEvent + IconError = v09.IconError + IconFastForward = v09.IconFastForward + IconFavorite = v09.IconFavorite + IconFavoriteOff = v09.IconFavoriteOff + IconFolder = v09.IconFolder + IconHelp = v09.IconHelp + IconHome = v09.IconHome + IconInfo = v09.IconInfo + IconLocationOn = v09.IconLocationOn + IconLock = v09.IconLock + IconLockOpen = v09.IconLockOpen + IconMail = v09.IconMail + IconMenu = v09.IconMenu + IconMoreVert = v09.IconMoreVert + IconMoreHoriz = v09.IconMoreHoriz + IconNotificationsOff = v09.IconNotificationsOff + IconNotifications = v09.IconNotifications + IconPause = v09.IconPause + IconPayment = v09.IconPayment + IconPerson = v09.IconPerson + IconPhone = v09.IconPhone + IconPhoto = v09.IconPhoto + IconPlay = v09.IconPlay + IconPrint = v09.IconPrint + IconRefresh = v09.IconRefresh + IconRewind = v09.IconRewind + IconSearch = v09.IconSearch + IconSend = v09.IconSend + IconSettings = v09.IconSettings + IconShare = v09.IconShare + IconShoppingCart = v09.IconShoppingCart + IconSkipNext = v09.IconSkipNext + IconSkipPrevious = v09.IconSkipPrevious + IconStar = v09.IconStar + IconStarHalf = v09.IconStarHalf + IconStarOff = v09.IconStarOff + IconStop = v09.IconStop + IconUpload = v09.IconUpload + IconVisibility = v09.IconVisibility + IconVisibilityOff = v09.IconVisibilityOff + IconVolumeDown = v09.IconVolumeDown + IconVolumeMute = v09.IconVolumeMute + IconVolumeOff = v09.IconVolumeOff + IconVolumeUp = v09.IconVolumeUp + IconWarning = v09.IconWarning +) + +// Enum constants. +const ( + ButtonVariantDefault = v09.ButtonVariantDefault + ButtonVariantPrimary = v09.ButtonVariantPrimary + ButtonVariantBorderless = v09.ButtonVariantBorderless + ChoicePickerDisplayStyleCheckbox = v09.ChoicePickerDisplayStyleCheckbox + ChoicePickerDisplayStyleChips = v09.ChoicePickerDisplayStyleChips + ChoicePickerVariantMultipleSelection = v09.ChoicePickerVariantMultipleSelection + ChoicePickerVariantMutuallyExclusive = v09.ChoicePickerVariantMutuallyExclusive + DividerAxisHorizontal = v09.DividerAxisHorizontal + DividerAxisVertical = v09.DividerAxisVertical + ImageFitContain = v09.ImageFitContain + ImageFitCover = v09.ImageFitCover + ImageFitFill = v09.ImageFitFill + ImageFitNone = v09.ImageFitNone + ImageFitScaleDown = v09.ImageFitScaleDown + ImageVariantIcon = v09.ImageVariantIcon + ImageVariantAvatar = v09.ImageVariantAvatar + ImageVariantSmallFeature = v09.ImageVariantSmallFeature + ImageVariantMediumFeature = v09.ImageVariantMediumFeature + ImageVariantLargeFeature = v09.ImageVariantLargeFeature + ImageVariantHeader = v09.ImageVariantHeader + LayoutAlignCenter = v09.LayoutAlignCenter + LayoutAlignEnd = v09.LayoutAlignEnd + LayoutAlignStart = v09.LayoutAlignStart + LayoutAlignStretch = v09.LayoutAlignStretch + LayoutJustifyStart = v09.LayoutJustifyStart + LayoutJustifyCenter = v09.LayoutJustifyCenter + LayoutJustifyEnd = v09.LayoutJustifyEnd + LayoutJustifySpaceBetween = v09.LayoutJustifySpaceBetween + LayoutJustifySpaceAround = v09.LayoutJustifySpaceAround + LayoutJustifySpaceEvenly = v09.LayoutJustifySpaceEvenly + LayoutJustifyStretch = v09.LayoutJustifyStretch + ListDirectionVertical = v09.ListDirectionVertical + ListDirectionHorizontal = v09.ListDirectionHorizontal + TextFieldVariantLongText = v09.TextFieldVariantLongText + TextFieldVariantNumber = v09.TextFieldVariantNumber + TextFieldVariantShortText = v09.TextFieldVariantShortText + TextFieldVariantObscured = v09.TextFieldVariantObscured + TextVariantH1 = v09.TextVariantH1 + TextVariantH2 = v09.TextVariantH2 + TextVariantH3 = v09.TextVariantH3 + TextVariantH4 = v09.TextVariantH4 + TextVariantH5 = v09.TextVariantH5 + TextVariantCaption = v09.TextVariantCaption + TextVariantBody = v09.TextVariantBody +) + +// Hand-written Dynamic* constructors. +var ( + StringLiteral = v09.StringLiteral + StringBinding = v09.StringBinding + StringFunc = v09.StringFunc + NumberLiteral = v09.NumberLiteral + NumberBinding = v09.NumberBinding + NumberFunc = v09.NumberFunc + BoolLiteral = v09.BoolLiteral + BoolBinding = v09.BoolBinding + BoolFunc = v09.BoolFunc + StringListLiteral = v09.StringListLiteral + StringListBinding = v09.StringListBinding + StringListFunc = v09.StringListFunc + ValueString = v09.ValueString + ValueNumber = v09.ValueNumber + ValueBool = v09.ValueBool + ValueArray = v09.ValueArray + ValueBinding = v09.ValueBinding + ValueFunc = v09.ValueFunc +) + +// Generated function constructors. +var ( + And = v09.And + Email = v09.Email + FormatCurrency = v09.FormatCurrency + FormatDate = v09.FormatDate + FormatNumber = v09.FormatNumber + FormatString = v09.FormatString + Length = v09.Length + Not = v09.Not + Numeric = v09.Numeric + OpenURL = v09.OpenURL + Or = v09.Or + Pluralize = v09.Pluralize + Regex = v09.Regex + Required = v09.Required +) diff --git a/agent_sdks/go/a2ui/doc.go b/agent_sdks/go/a2ui/doc.go new file mode 100644 index 0000000000..32768199ce --- /dev/null +++ b/agent_sdks/go/a2ui/doc.go @@ -0,0 +1,10 @@ +// Package a2ui provides Go types for the A2UI (Agent-to-User Interface) +// protocol, a declarative JSON format for AI agents to generate +// rich, interactive user interfaces. +// +// This package is the v0.9 compatibility API. It re-exports all types from +// [github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v09]. +// +// Version-specific code should import the subpackage directly, such as +// [github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010]. +package a2ui diff --git a/agent_sdks/go/a2ui/example_test.go b/agent_sdks/go/a2ui/example_test.go new file mode 100644 index 0000000000..7474123010 --- /dev/null +++ b/agent_sdks/go/a2ui/example_test.go @@ -0,0 +1,34 @@ +package a2ui_test + +import ( + "encoding/json" + "fmt" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" +) + +func Example() { + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "demo", + CatalogID: "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + }, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) + // Output: {"version":"v0.9","createSurface":{"surfaceId":"demo","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}} +} + +func ExampleComponent() { + comp := a2ui.Component{ + ID: "greeting", + Text: &a2ui.TextComponent{ + Text: a2ui.StringLiteral("Hello, world!"), + Variant: a2ui.TextVariantH1, + }, + } + data, _ := json.Marshal(comp) + fmt.Println(string(data)) + // Output: {"component":"Text","id":"greeting","text":"Hello, world!","variant":"h1"} +} diff --git a/agent_sdks/go/a2ui/v010/capabilities.go b/agent_sdks/go/a2ui/v010/capabilities.go new file mode 100644 index 0000000000..f37b05691f --- /dev/null +++ b/agent_sdks/go/a2ui/v010/capabilities.go @@ -0,0 +1,61 @@ +package v010 + +import "encoding/json" + +// CallableFrom describes where a catalog function may be invoked. +// Clients determine the execution boundary at runtime from the active catalog. +type CallableFrom string + +const ( + CallableFromClientOnly CallableFrom = "clientOnly" + CallableFromRemoteOnly CallableFrom = "remoteOnly" + CallableFromClientOrRemote CallableFrom = "clientOrRemote" +) + +// ClientCapabilities describes a client's UI rendering capabilities, +// sent as part of A2A metadata. +type ClientCapabilities struct { + V010 *ClientCapabilitiesV010 `json:"v0.10,omitempty"` +} + +// ClientCapabilitiesV010 is the v0.10 client capabilities structure. +type ClientCapabilitiesV010 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds"` + InlineCatalogs []CatalogDef `json:"inlineCatalogs,omitempty"` +} + +// ServerCapabilities describes an agent's supported UI features, +// advertised via agent card or other discovery. +type ServerCapabilities struct { + V010 *ServerCapabilitiesV010 `json:"v0.10,omitempty"` +} + +// ServerCapabilitiesV010 is the v0.10 server capabilities structure. +type ServerCapabilitiesV010 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds,omitempty"` + AcceptsInlineCatalogs bool `json:"acceptsInlineCatalogs,omitempty"` +} + +// CatalogDef is an inline catalog definition containing component schemas +// and function definitions. +type CatalogDef struct { + CatalogID string `json:"catalogId"` + Components map[string]json.RawMessage `json:"components,omitempty"` + Functions []FunctionDefinition `json:"functions,omitempty"` + Theme map[string]json.RawMessage `json:"theme,omitempty"` +} + +// FunctionDefinition describes a function's interface for catalog definitions. +type FunctionDefinition struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + CallableFrom CallableFrom `json:"callableFrom,omitempty"` + Parameters json.RawMessage `json:"parameters"` + ReturnType ReturnType `json:"returnType"` +} + +// ClientDataModel carries the client data model in A2A message metadata. +type ClientDataModel struct { + Version string `json:"version"` + Surfaces map[string]map[string]any `json:"surfaces"` +} diff --git a/agent_sdks/go/a2ui/v010/common.go b/agent_sdks/go/a2ui/v010/common.go new file mode 100644 index 0000000000..31edc68982 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/common.go @@ -0,0 +1,68 @@ +package v010 + +// DataBinding references a value in the client data model by JSON Pointer path. +type DataBinding struct { + Path string `json:"path"` +} + +// FunctionCall invokes a named client-side function. +type FunctionCall struct { + Call string `json:"call"` + Args map[string]any `json:"args,omitempty"` + ReturnType ReturnType `json:"returnType,omitempty"` +} + +// ChildList is either a static list of component IDs or a dynamic template. +// Exactly one of IDs or Template is set. +type ChildList struct { + IDs []string + Template *ChildTemplate +} + +// ChildTemplate generates a dynamic list of children from a data model list. +type ChildTemplate struct { + ComponentID string `json:"componentId"` + Path string `json:"path"` +} + +// CheckRule is a single validation rule applied to an input component. +type CheckRule struct { + Condition DynamicBoolean `json:"condition"` + Message string `json:"message"` +} + +// AccessibilityAttributes enhance accessibility for assistive technologies. +type AccessibilityAttributes struct { + Label *DynamicString `json:"label,omitempty"` + Description *DynamicString `json:"description,omitempty"` +} + +// Theme defines visual theming for a surface. +type Theme struct { + PrimaryColor string `json:"primaryColor,omitempty"` + IconURL string `json:"iconUrl,omitempty"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AdditionalProperties map[string]any `json:"-"` +} + +// Action is an interaction handler that either triggers a server-side event +// or executes a client-side function. Exactly one field is non-nil. +type Action struct { + Event *EventAction `json:"event,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` +} + +// EventAction triggers a server-side event. +type EventAction struct { + Name string `json:"name"` + Context map[string]DynamicValue `json:"context,omitempty"` + WantResponse bool `json:"wantResponse,omitempty"` + ResponsePath string `json:"responsePath,omitempty"` +} + +// IconNameOrPath is either a well-known icon name or a custom SVG path. +// Exactly one field is non-nil. +type IconNameOrPath struct { + Name *IconName + Path *string +} diff --git a/agent_sdks/go/a2ui/v010/common_json.go b/agent_sdks/go/a2ui/v010/common_json.go new file mode 100644 index 0000000000..7f5c809a1b --- /dev/null +++ b/agent_sdks/go/a2ui/v010/common_json.go @@ -0,0 +1,173 @@ +package v010 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for Theme. +func (t Theme) MarshalJSON() ([]byte, error) { + fields := make(map[string]any, len(t.AdditionalProperties)+3) + for k, v := range t.AdditionalProperties { + fields[k] = v + } + if t.PrimaryColor != "" { + fields["primaryColor"] = t.PrimaryColor + } + if t.IconURL != "" { + fields["iconUrl"] = t.IconURL + } + if t.AgentDisplayName != "" { + fields["agentDisplayName"] = t.AgentDisplayName + } + return json.Marshal(fields) +} + +// UnmarshalJSON implements json.Unmarshaler for Theme. +func (t *Theme) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return fmt.Errorf("a2ui: unmarshal theme: %w", err) + } + *t = Theme{} + for key, raw := range fields { + switch key { + case "primaryColor": + if err := json.Unmarshal(raw, &t.PrimaryColor); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.primaryColor: %w", err) + } + case "iconUrl": + if err := json.Unmarshal(raw, &t.IconURL); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.iconUrl: %w", err) + } + case "agentDisplayName": + if err := json.Unmarshal(raw, &t.AgentDisplayName); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.agentDisplayName: %w", err) + } + default: + if t.AdditionalProperties == nil { + t.AdditionalProperties = make(map[string]any) + } + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.%s: %w", key, err) + } + t.AdditionalProperties[key] = value + } + } + return nil +} + +// MarshalJSON implements json.Marshaler for ChildList. +func (c ChildList) MarshalJSON() ([]byte, error) { + if c.Template != nil && c.IDs != nil { + return nil, fmt.Errorf("a2ui: ChildList has both ids and template set") + } + switch { + case c.Template != nil: + return json.Marshal(c.Template) + case c.IDs != nil: + return json.Marshal(c.IDs) + default: + return []byte("[]"), nil + } +} + +// UnmarshalJSON implements json.Unmarshaler for ChildList. +func (c *ChildList) UnmarshalJSON(data []byte) error { + *c = ChildList{} + var ids []string + if err := json.Unmarshal(data, &ids); err == nil { + c.IDs = ids + return nil + } + var t ChildTemplate + if err := json.Unmarshal(data, &t); err != nil { + return fmt.Errorf("a2ui: unmarshal child list: %w", err) + } + c.Template = &t + return nil +} + +// MarshalJSON implements json.Marshaler for Action. +func (a Action) MarshalJSON() ([]byte, error) { + type actionAlias Action + switch countSet(a.Event != nil, a.FunctionCall != nil) { + case 1: + return json.Marshal(actionAlias(a)) + case 0: + return nil, fmt.Errorf("a2ui: Action has no value set") + default: + return nil, fmt.Errorf("a2ui: Action has multiple values set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for Action. +func (a *Action) UnmarshalJSON(data []byte) error { + type actionAlias Action + var aa actionAlias + if err := json.Unmarshal(data, &aa); err != nil { + return fmt.Errorf("a2ui: unmarshal action: %w", err) + } + switch countSet(aa.Event != nil, aa.FunctionCall != nil) { + case 1: + *a = Action(aa) + return nil + case 0: + return fmt.Errorf("a2ui: action must have event or functionCall") + default: + return fmt.Errorf("a2ui: action must not have both event and functionCall") + } +} + +// MarshalJSON implements json.Marshaler for IconNameOrPath. +func (i IconNameOrPath) MarshalJSON() ([]byte, error) { + switch countSet(i.Name != nil, i.Path != nil) { + case 1: + switch { + case i.Name != nil: + return json.Marshal(string(*i.Name)) + case i.Path != nil: + return json.Marshal(struct { + Path string `json:"path"` + }{Path: *i.Path}) + } + case 0: + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") + default: + return nil, fmt.Errorf("a2ui: IconNameOrPath has multiple values set") + } + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") +} + +// UnmarshalJSON implements json.Unmarshaler for IconNameOrPath. +func (i *IconNameOrPath) UnmarshalJSON(data []byte) error { + *i = IconNameOrPath{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + name := IconName(s) + i.Name = &name + return nil + } + var obj struct { + Path string `json:"path"` + } + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: unmarshal icon name or path: %w", err) + } + if obj.Path == "" { + return fmt.Errorf("a2ui: icon path must not be empty") + } + i.Path = &obj.Path + return nil +} + +func countSet(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} diff --git a/agent_sdks/go/a2ui/v010/common_test.go b/agent_sdks/go/a2ui/v010/common_test.go new file mode 100644 index 0000000000..b2d18dfb4b --- /dev/null +++ b/agent_sdks/go/a2ui/v010/common_test.go @@ -0,0 +1,63 @@ +package v010 + +import ( + "encoding/json" + "testing" +) + +func TestActionRejectsInvalidStates(t *testing.T) { + action := Action{ + Event: &EventAction{Name: "submit"}, + FunctionCall: &FunctionCall{Call: "openUrl"}, + } + if _, err := json.Marshal(action); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded Action + if err := json.Unmarshal([]byte(`{"event":{"name":"submit"},"functionCall":{"call":"openUrl"}}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestIconNameOrPathRejectsInvalidStates(t *testing.T) { + path := "/tmp/icon.svg" + name := IconSearch + icon := IconNameOrPath{Name: &name, Path: &path} + if _, err := json.Marshal(icon); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded IconNameOrPath + if err := json.Unmarshal([]byte(`{"path":""}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestChildListRejectsMultipleRepresentations(t *testing.T) { + children := ChildList{ + IDs: []string{"a"}, + Template: &ChildTemplate{ComponentID: "child", Path: "/items"}, + } + if _, err := json.Marshal(children); err == nil { + t.Fatal("expected marshal error, got nil") + } +} + +func TestThemePreservesAdditionalProperties(t *testing.T) { + data := []byte(`{"primaryColor":"#fff","customProperty":"customValue","nested":{"enabled":true}}`) + var theme Theme + if err := json.Unmarshal(data, &theme); err != nil { + t.Fatal(err) + } + if theme.PrimaryColor != "#fff" { + t.Fatalf("PrimaryColor = %q, want #fff", theme.PrimaryColor) + } + if got := theme.AdditionalProperties["customProperty"]; got != "customValue" { + t.Fatalf("customProperty = %#v, want customValue", got) + } + if _, ok := theme.AdditionalProperties["nested"].(map[string]any); !ok { + t.Fatalf("nested = %#v, want object", theme.AdditionalProperties["nested"]) + } + jsonEquivalent(t, data, theme) +} diff --git a/agent_sdks/go/a2ui/v010/component.go b/agent_sdks/go/a2ui/v010/component.go new file mode 100644 index 0000000000..21f7e031b6 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/component.go @@ -0,0 +1,110 @@ +package v010 + +// Component represents any A2UI component in the component tree. +// Exactly one of the concrete type fields is non-nil. +// +// MarshalJSON/UnmarshalJSON in zz_component_marshal.go handle +// serialization, using the "component" field as a discriminator. +type Component struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + + // Concrete type fields (exactly one non-nil). + Text *TextComponent `json:"-"` + Image *ImageComponent `json:"-"` + Icon *IconComponent `json:"-"` + Video *VideoComponent `json:"-"` + AudioPlayer *AudioPlayerComponent `json:"-"` + Row *RowComponent `json:"-"` + Column *ColumnComponent `json:"-"` + List *ListComponent `json:"-"` + Card *CardComponent `json:"-"` + Tabs *TabsComponent `json:"-"` + Modal *ModalComponent `json:"-"` + Divider *DividerComponent `json:"-"` + Button *ButtonComponent `json:"-"` + TextField *TextFieldComponent `json:"-"` + CheckBox *CheckBoxComponent `json:"-"` + ChoicePicker *ChoicePickerComponent `json:"-"` + Slider *SliderComponent `json:"-"` + DateTimeInput *DateTimeInputComponent `json:"-"` +} + +func (c Component) componentData() (string, any, int) { + var ( + componentType string + specific any + count int + ) + set := func(typ string, value any) { + componentType = typ + specific = value + count++ + } + if c.Text != nil { + set("Text", c.Text) + } + if c.Image != nil { + set("Image", c.Image) + } + if c.Icon != nil { + set("Icon", c.Icon) + } + if c.Video != nil { + set("Video", c.Video) + } + if c.AudioPlayer != nil { + set("AudioPlayer", c.AudioPlayer) + } + if c.Row != nil { + set("Row", c.Row) + } + if c.Column != nil { + set("Column", c.Column) + } + if c.List != nil { + set("List", c.List) + } + if c.Card != nil { + set("Card", c.Card) + } + if c.Tabs != nil { + set("Tabs", c.Tabs) + } + if c.Modal != nil { + set("Modal", c.Modal) + } + if c.Divider != nil { + set("Divider", c.Divider) + } + if c.Button != nil { + set("Button", c.Button) + } + if c.TextField != nil { + set("TextField", c.TextField) + } + if c.CheckBox != nil { + set("CheckBox", c.CheckBox) + } + if c.ChoicePicker != nil { + set("ChoicePicker", c.ChoicePicker) + } + if c.Slider != nil { + set("Slider", c.Slider) + } + if c.DateTimeInput != nil { + set("DateTimeInput", c.DateTimeInput) + } + return componentType, specific, count +} + +// ComponentType returns the discriminator string (e.g. "Text", "Button"). +func (c Component) ComponentType() string { + componentType, _, count := c.componentData() + if count != 1 { + return "" + } + return componentType +} diff --git a/agent_sdks/go/a2ui/v010/component_test.go b/agent_sdks/go/a2ui/v010/component_test.go new file mode 100644 index 0000000000..b1da9d3a1f --- /dev/null +++ b/agent_sdks/go/a2ui/v010/component_test.go @@ -0,0 +1,254 @@ +package v010 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +const basicExamplesDir = "testdata/v0_10/catalogs/basic/examples" + +func TestLoginFormComponents(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/09_login-form.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + + // Second message is updateComponents. + var msg ServerMessage + if err := json.Unmarshal(example.Messages[1], &msg); err != nil { + t.Fatal(err) + } + if msg.UpdateComponents == nil { + t.Fatal("expected updateComponents") + } + + components := msg.UpdateComponents.Components + byID := make(map[string]*Component, len(components)) + for i := range components { + byID[components[i].ID] = &components[i] + } + + t.Run("TextField", func(t *testing.T) { + c, ok := byID["email-field"] + if !ok { + t.Fatal("missing email-field") + } + if c.ComponentType() != "TextField" { + t.Fatalf("type = %q, want TextField", c.ComponentType()) + } + if c.TextField == nil { + t.Fatal("TextField is nil") + } + if c.TextField.Label.Literal == nil || *c.TextField.Label.Literal != "Email" { + t.Fatalf("label = %+v, want literal Email", c.TextField.Label) + } + if c.TextField.Value == nil || c.TextField.Value.Binding == nil || c.TextField.Value.Binding.Path != "/email" { + t.Fatal("value should bind to /email") + } + }) + + t.Run("Button", func(t *testing.T) { + c, ok := byID["login-btn"] + if !ok { + t.Fatal("missing login-btn") + } + if c.ComponentType() != "Button" { + t.Fatalf("type = %q, want Button", c.ComponentType()) + } + if c.Button.Child != "login-btn-text" { + t.Fatalf("child = %q", c.Button.Child) + } + if c.Button.Action.Event == nil { + t.Fatal("expected event action") + } + if c.Button.Action.Event.Name != "login" { + t.Fatalf("event name = %q", c.Button.Action.Event.Name) + } + }) + + t.Run("Column", func(t *testing.T) { + c, ok := byID["main-column"] + if !ok { + t.Fatal("missing main-column") + } + if c.ComponentType() != "Column" { + t.Fatalf("type = %q, want Column", c.ComponentType()) + } + if len(c.Column.Children.IDs) != 6 { + t.Fatalf("children = %d, want 6", len(c.Column.Children.IDs)) + } + }) + + t.Run("CheckRule", func(t *testing.T) { + c := byID["email-field"] + if len(c.Checks) != 2 { + t.Fatalf("checks = %d, want 2", len(c.Checks)) + } + if c.Checks[0].Message != "Email is required" { + t.Fatalf("message = %q", c.Checks[0].Message) + } + if c.Checks[0].Condition.FunctionCall == nil { + t.Fatal("expected function call condition") + } + if c.Checks[0].Condition.FunctionCall.Call != "required" { + t.Fatalf("call = %q", c.Checks[0].Condition.FunctionCall.Call) + } + }) + + t.Run("Card", func(t *testing.T) { + c, ok := byID["root"] + if !ok { + t.Fatal("missing root") + } + if c.ComponentType() != "Card" { + t.Fatalf("type = %q, want Card", c.ComponentType()) + } + if c.Card.Child != "main-column" { + t.Fatalf("child = %q", c.Card.Child) + } + }) +} + +func TestComponentTypeDiscriminator(t *testing.T) { + tests := []struct { + name string + comp Component + want string + }{ + {"Text", Component{Text: &TextComponent{}}, "Text"}, + {"Button", Component{Button: &ButtonComponent{}}, "Button"}, + {"Column", Component{Column: &ColumnComponent{}}, "Column"}, + {"Row", Component{Row: &RowComponent{}}, "Row"}, + {"Card", Component{Card: &CardComponent{}}, "Card"}, + {"Image", Component{Image: &ImageComponent{}}, "Image"}, + {"Icon", Component{Icon: &IconComponent{}}, "Icon"}, + {"TextField", Component{TextField: &TextFieldComponent{}}, "TextField"}, + {"CheckBox", Component{CheckBox: &CheckBoxComponent{}}, "CheckBox"}, + {"Divider", Component{Divider: &DividerComponent{}}, "Divider"}, + {"Slider", Component{Slider: &SliderComponent{}}, "Slider"}, + {"Tabs", Component{Tabs: &TabsComponent{}}, "Tabs"}, + {"Modal", Component{Modal: &ModalComponent{}}, "Modal"}, + {"List", Component{List: &ListComponent{}}, "List"}, + {"multiple", Component{Text: &TextComponent{}, Button: &ButtonComponent{}}, ""}, + {"empty", Component{}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.comp.ComponentType(); got != tt.want { + t.Fatalf("ComponentType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestComponentRoundTrip(t *testing.T) { + tests := []struct { + name string + json string + }{ + { + name: "text", + json: `{"component":"Text","id":"t1","text":"hello","variant":"h1"}`, + }, + { + name: "button_with_event", + json: `{"component":"Button","id":"b1","child":"b1-text","action":{"event":{"name":"click"}}}`, + }, + { + name: "column", + json: `{"component":"Column","id":"c1","children":["a","b","c"],"align":"center"}`, + }, + { + name: "text_with_binding", + json: `{"component":"Text","id":"t2","text":{"path":"/name"}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var c Component + if err := json.Unmarshal([]byte(tt.json), &c); err != nil { + t.Fatalf("unmarshal: %v", err) + } + roundTrip(t, c, tt.json) + }) + } +} + +func TestComponentMarshalRejectsInvalidConcreteTypes(t *testing.T) { + tests := []struct { + name string + comp Component + }{ + { + name: "none", + comp: Component{ID: "empty"}, + }, + { + name: "multiple", + comp: Component{ + ID: "bad", + Text: &TextComponent{Text: StringLiteral("hello")}, + Button: &ButtonComponent{Action: Action{Event: &EventAction{Name: "click"}}, Child: "child"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.comp); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestAllExamplesUnmarshal(t *testing.T) { + entries, err := os.ReadDir(basicExamplesDir) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + t.Run(e.Name(), func(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/" + e.Name()) + if err != nil { + t.Fatal(err) + } + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + for i, raw := range example.Messages { + var msg ServerMessage + if err := json.Unmarshal(raw, &msg); err != nil { + t.Fatalf("message[%d]: unmarshal: %v", i, err) + } + remarshaled, err := json.Marshal(msg) + if err != nil { + t.Fatalf("message[%d]: re-marshal: %v", i, err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("message[%d]: unmarshal re-marshaled: %v", i, err) + } + if err := json.Unmarshal(raw, &want); err != nil { + t.Fatalf("message[%d]: unmarshal original: %v", i, err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("message[%d]: round-trip mismatch", i) + } + } + }) + } +} diff --git a/agent_sdks/go/a2ui/v010/doc.go b/agent_sdks/go/a2ui/v010/doc.go new file mode 100644 index 0000000000..f67cc9307d --- /dev/null +++ b/agent_sdks/go/a2ui/v010/doc.go @@ -0,0 +1,2 @@ +// Package v010 provides Go types for the A2UI protocol v0.10. +package v010 diff --git a/agent_sdks/go/a2ui/v010/dynamic.go b/agent_sdks/go/a2ui/v010/dynamic.go new file mode 100644 index 0000000000..b49cbc28b3 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/dynamic.go @@ -0,0 +1,120 @@ +package v010 + +// DynamicString represents a string that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicString struct { + Literal *string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringLiteral creates a DynamicString from a literal string value. +func StringLiteral(s string) DynamicString { return DynamicString{Literal: &s} } + +// StringBinding creates a DynamicString from a data model path. +func StringBinding(path string) DynamicString { + return DynamicString{Binding: &DataBinding{Path: path}} +} + +// StringFunc creates a DynamicString from a function call. +func StringFunc(call FunctionCall) DynamicString { + return DynamicString{FunctionCall: &call} +} + +// DynamicNumber represents a number that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicNumber struct { + Literal *float64 + Binding *DataBinding + FunctionCall *FunctionCall +} + +// NumberLiteral creates a DynamicNumber from a literal float64 value. +func NumberLiteral(n float64) DynamicNumber { return DynamicNumber{Literal: &n} } + +// NumberBinding creates a DynamicNumber from a data model path. +func NumberBinding(path string) DynamicNumber { + return DynamicNumber{Binding: &DataBinding{Path: path}} +} + +// NumberFunc creates a DynamicNumber from a function call. +func NumberFunc(call FunctionCall) DynamicNumber { + return DynamicNumber{FunctionCall: &call} +} + +// DynamicBoolean represents a boolean that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicBoolean struct { + Literal *bool + Binding *DataBinding + FunctionCall *FunctionCall +} + +// BoolLiteral creates a DynamicBoolean from a literal bool value. +func BoolLiteral(b bool) DynamicBoolean { return DynamicBoolean{Literal: &b} } + +// BoolBinding creates a DynamicBoolean from a data model path. +func BoolBinding(path string) DynamicBoolean { + return DynamicBoolean{Binding: &DataBinding{Path: path}} +} + +// BoolFunc creates a DynamicBoolean from a function call. +func BoolFunc(call FunctionCall) DynamicBoolean { + return DynamicBoolean{FunctionCall: &call} +} + +// DynamicStringList represents a string list that can be a literal, a data +// binding, or a function call. Exactly one field is non-nil. +type DynamicStringList struct { + Literal []string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringListLiteral creates a DynamicStringList from literal string values. +func StringListLiteral(ss []string) DynamicStringList { + return DynamicStringList{Literal: ss} +} + +// StringListBinding creates a DynamicStringList from a data model path. +func StringListBinding(path string) DynamicStringList { + return DynamicStringList{Binding: &DataBinding{Path: path}} +} + +// StringListFunc creates a DynamicStringList from a function call. +func StringListFunc(call FunctionCall) DynamicStringList { + return DynamicStringList{FunctionCall: &call} +} + +// DynamicValue represents a value of any type: string, number, boolean, array, +// data binding, or function call. Exactly one field is non-nil. +type DynamicValue struct { + String *string + Number *float64 + Bool *bool + Array []any + Binding *DataBinding + FunctionCall *FunctionCall +} + +// ValueString creates a DynamicValue from a string. +func ValueString(s string) DynamicValue { return DynamicValue{String: &s} } + +// ValueNumber creates a DynamicValue from a number. +func ValueNumber(n float64) DynamicValue { return DynamicValue{Number: &n} } + +// ValueBool creates a DynamicValue from a boolean. +func ValueBool(b bool) DynamicValue { return DynamicValue{Bool: &b} } + +// ValueArray creates a DynamicValue from an array. +func ValueArray(a []any) DynamicValue { return DynamicValue{Array: a} } + +// ValueBinding creates a DynamicValue from a data model path. +func ValueBinding(path string) DynamicValue { + return DynamicValue{Binding: &DataBinding{Path: path}} +} + +// ValueFunc creates a DynamicValue from a function call. +func ValueFunc(call FunctionCall) DynamicValue { + return DynamicValue{FunctionCall: &call} +} diff --git a/agent_sdks/go/a2ui/v010/dynamic_json.go b/agent_sdks/go/a2ui/v010/dynamic_json.go new file mode 100644 index 0000000000..18f3f8ac8e --- /dev/null +++ b/agent_sdks/go/a2ui/v010/dynamic_json.go @@ -0,0 +1,239 @@ +package v010 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for DynamicString. +func (d DynamicString) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicString has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicString has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicString. +func (d *DynamicString) UnmarshalJSON(data []byte) error { + *d = DynamicString{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.Literal = &s + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicNumber. +func (d DynamicNumber) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicNumber has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicNumber has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicNumber. +func (d *DynamicNumber) UnmarshalJSON(data []byte) error { + *d = DynamicNumber{} + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Literal = &n + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicBoolean. +func (d DynamicBoolean) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicBoolean has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicBoolean has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicBoolean. +func (d *DynamicBoolean) UnmarshalJSON(data []byte) error { + *d = DynamicBoolean{} + var b bool + if err := json.Unmarshal(data, &b); err == nil { + d.Literal = &b + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicStringList. +func (d DynamicStringList) MarshalJSON() ([]byte, error) { + if count := countSliceValues(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicStringList has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicStringList has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicStringList. +func (d *DynamicStringList) UnmarshalJSON(data []byte) error { + *d = DynamicStringList{} + var ss []string + if err := json.Unmarshal(data, &ss); err == nil { + d.Literal = ss + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicValue. +func (d DynamicValue) MarshalJSON() ([]byte, error) { + if count := countDynamicValueFields(d); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicValue has multiple values set") + } + switch { + case d.String != nil: + return json.Marshal(*d.String) + case d.Number != nil: + return json.Marshal(*d.Number) + case d.Bool != nil: + return json.Marshal(*d.Bool) + case d.Array != nil: + return json.Marshal(d.Array) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicValue has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicValue. +func (d *DynamicValue) UnmarshalJSON(data []byte) error { + *d = DynamicValue{} + data = bytes.TrimSpace(data) + // Try string. + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.String = &s + return nil + } + // Try bool (before number, since Go's json decoder doesn't confuse them, + // but we check bool first for clarity). + var b bool + if err := json.Unmarshal(data, &b); err == nil { + if len(data) > 0 && (data[0] == 't' || data[0] == 'f') { + d.Bool = &b + return nil + } + } + // Try number. + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Number = &n + return nil + } + // Try array. + var arr []any + if err := json.Unmarshal(data, &arr); err == nil { + d.Array = arr + return nil + } + // Must be an object: binding or function call. + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// unmarshalBindingOrFunc tries to unmarshal data as a DataBinding (has "path" +// key) or a FunctionCall (has "call" key). +func unmarshalBindingOrFunc(data []byte, binding **DataBinding, fn **FunctionCall) error { + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: cannot unmarshal dynamic value: %w", err) + } + if _, ok := obj["path"]; ok { + if _, ok := obj["call"]; ok { + return fmt.Errorf("a2ui: object cannot be both a data binding and a function call") + } + var db DataBinding + if err := json.Unmarshal(data, &db); err != nil { + return fmt.Errorf("a2ui: unmarshal data binding: %w", err) + } + *binding = &db + return nil + } + if _, ok := obj["call"]; ok { + var fc FunctionCall + if err := json.Unmarshal(data, &fc); err != nil { + return fmt.Errorf("a2ui: unmarshal function call: %w", err) + } + *fn = &fc + return nil + } + return fmt.Errorf("a2ui: object is neither a data binding nor a function call") +} + +func countSliceValues(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} + +func countDynamicValueFields(d DynamicValue) int { + var count int + if d.String != nil { + count++ + } + if d.Number != nil { + count++ + } + if d.Bool != nil { + count++ + } + if d.Array != nil { + count++ + } + if d.Binding != nil { + count++ + } + if d.FunctionCall != nil { + count++ + } + return count +} diff --git a/agent_sdks/go/a2ui/v010/dynamic_test.go b/agent_sdks/go/a2ui/v010/dynamic_test.go new file mode 100644 index 0000000000..42437337b7 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/dynamic_test.go @@ -0,0 +1,308 @@ +package v010 + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestDynamicString(t *testing.T) { + tests := []struct { + name string + json string + want DynamicString + }{ + { + name: "literal", + json: `"hello"`, + want: StringLiteral("hello"), + }, + { + name: "binding", + json: `{"path":"/foo"}`, + want: StringBinding("/foo"), + }, + { + name: "function_call", + json: `{"call":"formatString","args":{"value":"hi"},"returnType":"string"}`, + want: StringFunc(FunctionCall{ + Call: "formatString", + Args: map[string]any{"value": "hi"}, + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicNumber(t *testing.T) { + tests := []struct { + name string + json string + want DynamicNumber + }{ + { + name: "literal", + json: `42.5`, + want: NumberLiteral(42.5), + }, + { + name: "integer", + json: `100`, + want: NumberLiteral(100), + }, + { + name: "binding", + json: `{"path":"/count"}`, + want: NumberBinding("/count"), + }, + { + name: "function_call", + json: `{"call":"add","args":{"a":1,"b":2},"returnType":"number"}`, + want: NumberFunc(FunctionCall{ + Call: "add", + Args: map[string]any{"a": float64(1), "b": float64(2)}, + ReturnType: ReturnTypeNumber, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicNumber + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicBoolean(t *testing.T) { + tests := []struct { + name string + json string + want DynamicBoolean + }{ + { + name: "true", + json: `true`, + want: BoolLiteral(true), + }, + { + name: "false", + json: `false`, + want: BoolLiteral(false), + }, + { + name: "binding", + json: `{"path":"/enabled"}`, + want: BoolBinding("/enabled"), + }, + { + name: "function_call", + json: `{"call":"required","args":{"value":"x"},"returnType":"boolean"}`, + want: BoolFunc(FunctionCall{ + Call: "required", + Args: map[string]any{"value": "x"}, + ReturnType: ReturnTypeBoolean, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicBoolean + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicStringList(t *testing.T) { + tests := []struct { + name string + json string + want DynamicStringList + }{ + { + name: "literal", + json: `["a","b"]`, + want: StringListLiteral([]string{"a", "b"}), + }, + { + name: "binding", + json: `{"path":"/tags"}`, + want: StringListBinding("/tags"), + }, + { + name: "function_call", + json: `{"call":"split","args":{"sep":","},"returnType":"array"}`, + want: StringListFunc(FunctionCall{ + Call: "split", + Args: map[string]any{"sep": ","}, + ReturnType: ReturnTypeArray, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicStringList + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicValue(t *testing.T) { + tests := []struct { + name string + json string + want DynamicValue + }{ + { + name: "string", + json: `"hello"`, + want: ValueString("hello"), + }, + { + name: "number", + json: `3.14`, + want: ValueNumber(3.14), + }, + { + name: "bool_true", + json: `true`, + want: ValueBool(true), + }, + { + name: "bool_false", + json: `false`, + want: ValueBool(false), + }, + { + name: "array", + json: `[1,"two",true]`, + want: ValueArray([]any{float64(1), "two", true}), + }, + { + name: "binding", + json: `{"path":"/data"}`, + want: ValueBinding("/data"), + }, + { + name: "function_call", + json: `{"call":"now","returnType":"string"}`, + want: ValueFunc(FunctionCall{ + Call: "now", + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicValue + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicMarshalEmpty(t *testing.T) { + tests := []struct { + name string + fn func() ([]byte, error) + }{ + {"DynamicString", func() ([]byte, error) { return json.Marshal(DynamicString{}) }}, + {"DynamicNumber", func() ([]byte, error) { return json.Marshal(DynamicNumber{}) }}, + {"DynamicBoolean", func() ([]byte, error) { return json.Marshal(DynamicBoolean{}) }}, + {"DynamicStringList", func() ([]byte, error) { return json.Marshal(DynamicStringList{}) }}, + {"DynamicValue", func() ([]byte, error) { return json.Marshal(DynamicValue{}) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.fn() + if err == nil { + t.Fatal("expected error for zero-value marshal, got nil") + } + }) + } +} + +func TestDynamicRejectsMultipleValues(t *testing.T) { + s := "hello" + tests := []struct { + name string + value any + }{ + {"DynamicString", DynamicString{Literal: &s, Binding: &DataBinding{Path: "/name"}}}, + {"DynamicNumber", DynamicNumber{Literal: float64Ptr(42), Binding: &DataBinding{Path: "/count"}}}, + {"DynamicBoolean", DynamicBoolean{Literal: boolPtr(true), Binding: &DataBinding{Path: "/enabled"}}}, + {"DynamicStringList", DynamicStringList{Literal: []string{"a"}, Binding: &DataBinding{Path: "/tags"}}}, + {"DynamicValue", DynamicValue{String: &s, Binding: &DataBinding{Path: "/value"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.value); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestDynamicRejectsAmbiguousObject(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(`{"path":"/name","call":"formatString"}`), &got); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +// roundTrip marshals v, then verifies the JSON is semantically equivalent to wantJSON. +func roundTrip(t *testing.T, v any, wantJSON string) { + t.Helper() + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal marshaled: %v", err) + } + if err := json.Unmarshal([]byte(wantJSON), &want); err != nil { + t.Fatalf("unmarshal want: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round-trip mismatch:\n got: %s\n want: %s", data, wantJSON) + } +} + +func boolPtr(v bool) *bool { return &v } + +func float64Ptr(v float64) *float64 { return &v } diff --git a/agent_sdks/go/a2ui/v010/example_test.go b/agent_sdks/go/a2ui/v010/example_test.go new file mode 100644 index 0000000000..61b97631a0 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/example_test.go @@ -0,0 +1,63 @@ +package v010_test + +import ( + "encoding/json" + "fmt" + + v010 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010" +) + +func Example() { + msg := v010.ServerMessage{ + Version: v010.Version, + CreateSurface: &v010.CreateSurface{ + SurfaceID: "demo", + CatalogID: "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + }, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) + // Output: {"version":"v0.10","createSurface":{"surfaceId":"demo","catalogId":"https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json"}} +} + +func ExampleComponent() { + comp := v010.Component{ + ID: "greeting", + Text: &v010.TextComponent{ + Text: v010.StringLiteral("Hello, world!"), + Variant: v010.TextVariantH1, + }, + } + data, _ := json.Marshal(comp) + fmt.Println(string(data)) + // Output: {"component":"Text","id":"greeting","text":"Hello, world!","variant":"h1"} +} + +func ExampleDynamicString() { + // Literal string. + lit := v010.StringLiteral("hello") + data, _ := json.Marshal(lit) + fmt.Println(string(data)) + + // Data binding. + bind := v010.StringBinding("/user/name") + data, _ = json.Marshal(bind) + fmt.Println(string(data)) + // Output: + // "hello" + // {"path":"/user/name"} +} + +func ExampleDynamicNumber() { + n := v010.NumberLiteral(42) + data, _ := json.Marshal(n) + fmt.Println(string(data)) + // Output: 42 +} + +func ExampleDynamicBoolean() { + b := v010.BoolBinding("/settings/enabled") + data, _ := json.Marshal(b) + fmt.Println(string(data)) + // Output: {"path":"/settings/enabled"} +} diff --git a/agent_sdks/go/a2ui/v010/gen.go b/agent_sdks/go/a2ui/v010/gen.go new file mode 100644 index 0000000000..5ac5588637 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/gen.go @@ -0,0 +1,3 @@ +package v010 + +//go:generate go run ../../cmd/a2uigen -schemas=../../../../specification/v0_10/json -pkg=v010 -out=../.. diff --git a/agent_sdks/go/a2ui/v010/message.go b/agent_sdks/go/a2ui/v010/message.go new file mode 100644 index 0000000000..c183a432b0 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/message.go @@ -0,0 +1,105 @@ +package v010 + +// Version is the A2UI protocol version implemented by this package. +const Version = "v0.10" + +// ServerMessage is a message sent from the agent to the renderer. +// Exactly one of the payload fields is non-nil. +type ServerMessage struct { + Version string `json:"version"` + FunctionCallID string `json:"functionCallId,omitempty"` + ActionID string `json:"actionId,omitempty"` + WantResponse bool `json:"wantResponse,omitempty"` + CreateSurface *CreateSurface `json:"createSurface,omitempty"` + UpdateComponents *UpdateComponents `json:"updateComponents,omitempty"` + UpdateDataModel *UpdateDataModel `json:"updateDataModel,omitempty"` + DeleteSurface *DeleteSurface `json:"deleteSurface,omitempty"` + CallFunction *FunctionCall `json:"callFunction,omitempty"` + ActionResponse *ActionResponse `json:"actionResponse,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ServerMessage) VersionString() string { return m.Version } + +// CreateSurface signals the client to create a new surface. +type CreateSurface struct { + SurfaceID string `json:"surfaceId"` + CatalogID string `json:"catalogId"` + Theme *Theme `json:"theme,omitempty"` + SendDataModel bool `json:"sendDataModel,omitempty"` +} + +// UpdateComponents updates a surface with a new set of components. +type UpdateComponents struct { + SurfaceID string `json:"surfaceId"` + Components []Component `json:"components"` +} + +// UpdateDataModel updates the data model for a surface. +type UpdateDataModel struct { + SurfaceID string `json:"surfaceId"` + Path string `json:"path,omitempty"` + Value any `json:"value,omitempty"` +} + +// DeleteSurface signals the client to delete a surface. +type DeleteSurface struct { + SurfaceID string `json:"surfaceId"` +} + +// ActionResponse is a response to a client-initiated action. +type ActionResponse struct { + Value any `json:"-"` + HasValue bool `json:"-"` + Error *ActionResponseError `json:"-"` +} + +// ActionResponseValue returns an action response with a value, including nil. +func ActionResponseValue(value any) ActionResponse { + return ActionResponse{Value: value, HasValue: true} +} + +// ActionResponseError reports a failed action response. +type ActionResponseError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// ClientMessage is a message sent from the renderer to the agent. +// Exactly one of Action, FunctionResponse, or Error is non-nil. +type ClientMessage struct { + Version string `json:"version"` + Action *ActionEvent `json:"action,omitempty"` + FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` + Error *ClientError `json:"error,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ClientMessage) VersionString() string { return m.Version } + +// ActionEvent reports a user-initiated action from a component. +type ActionEvent struct { + Name string `json:"name"` + SurfaceID string `json:"surfaceId"` + SourceComponentID string `json:"sourceComponentId"` + Timestamp string `json:"timestamp"` + Context map[string]any `json:"context"` + WantResponse bool `json:"wantResponse,omitempty"` + ActionID string `json:"actionId,omitempty"` +} + +// FunctionResponse reports the result of a server-initiated function call. +type FunctionResponse struct { + FunctionCallID string `json:"functionCallId"` + Call string `json:"call"` + Value any `json:"value"` +} + +// ClientError reports a client-side error. +type ClientError struct { + Code string `json:"code"` + SurfaceID string `json:"surfaceId,omitempty"` + FunctionCallID string `json:"functionCallId,omitempty"` + Message string `json:"message"` + Path string `json:"path,omitempty"` +} diff --git a/agent_sdks/go/a2ui/v010/message_json.go b/agent_sdks/go/a2ui/v010/message_json.go new file mode 100644 index 0000000000..45b52377fa --- /dev/null +++ b/agent_sdks/go/a2ui/v010/message_json.go @@ -0,0 +1,166 @@ +package v010 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for ServerMessage. +func (m ServerMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ServerMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ServerMessage. +func (m *ServerMessage) UnmarshalJSON(data []byte) error { + type alias ServerMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal server message: %w", err) + } + msg := ServerMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ServerMessage) validate() error { + switch countSet(m.CreateSurface != nil, m.UpdateComponents != nil, m.UpdateDataModel != nil, m.DeleteSurface != nil, m.CallFunction != nil, m.ActionResponse != nil) { + case 1: + case 0: + return fmt.Errorf("a2ui: server message has no payload set") + default: + return fmt.Errorf("a2ui: server message has multiple payloads set") + } + if m.CallFunction != nil { + if m.FunctionCallID == "" { + return fmt.Errorf("a2ui: call function message functionCallId is required") + } + if m.CallFunction.Call == "" { + return fmt.Errorf("a2ui: call function call is required") + } + if m.CallFunction.ReturnType == "" { + return fmt.Errorf("a2ui: call function returnType is required") + } + } + if m.ActionResponse != nil && m.ActionID == "" { + return fmt.Errorf("a2ui: action response message actionId is required") + } + return nil +} + +// MarshalJSON implements json.Marshaler for ActionResponse. +func (r ActionResponse) MarshalJSON() ([]byte, error) { + hasValue := r.HasValue || r.Value != nil + hasError := r.Error != nil + switch { + case hasValue && hasError: + return nil, fmt.Errorf("a2ui: action response has both value and error set") + case hasValue: + return json.Marshal(struct { + Value any `json:"value"` + }{Value: r.Value}) + case hasError: + return json.Marshal(struct { + Error *ActionResponseError `json:"error"` + }{Error: r.Error}) + default: + return nil, fmt.Errorf("a2ui: action response has no value or error set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for ActionResponse. +func (r *ActionResponse) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return fmt.Errorf("a2ui: unmarshal action response: %w", err) + } + valueData, hasValue := fields["value"] + errorData, hasError := fields["error"] + switch { + case hasValue && hasError: + return fmt.Errorf("a2ui: action response must not have both value and error") + case hasValue: + var value any + if string(bytes.TrimSpace(valueData)) != "null" { + if err := json.Unmarshal(valueData, &value); err != nil { + return fmt.Errorf("a2ui: unmarshal action response value: %w", err) + } + } + *r = ActionResponse{Value: value, HasValue: true} + return nil + case hasError: + var responseError ActionResponseError + if err := json.Unmarshal(errorData, &responseError); err != nil { + return fmt.Errorf("a2ui: unmarshal action response error: %w", err) + } + *r = ActionResponse{Error: &responseError} + return nil + default: + return fmt.Errorf("a2ui: action response must have value or error") + } +} + +// MarshalJSON implements json.Marshaler for ClientMessage. +func (m ClientMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ClientMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ClientMessage. +func (m *ClientMessage) UnmarshalJSON(data []byte) error { + type alias ClientMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal client message: %w", err) + } + msg := ClientMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ClientMessage) validate() error { + switch countSet(m.Action != nil, m.FunctionResponse != nil, m.Error != nil) { + case 1: + case 0: + return fmt.Errorf("a2ui: client message has no payload set") + default: + return fmt.Errorf("a2ui: client message has multiple payloads set") + } + if m.Error != nil { + if m.Error.Code == "" { + return fmt.Errorf("a2ui: client error code is required") + } + if m.Error.Message == "" { + return fmt.Errorf("a2ui: client error message is required") + } + switch countSet(m.Error.SurfaceID != "", m.Error.FunctionCallID != "") { + case 1: + case 0: + return fmt.Errorf("a2ui: client error must have surfaceId or functionCallId") + default: + return fmt.Errorf("a2ui: client error must not have both surfaceId and functionCallId") + } + } + if m.FunctionResponse != nil { + if m.FunctionResponse.FunctionCallID == "" { + return fmt.Errorf("a2ui: function response functionCallId is required") + } + if m.FunctionResponse.Call == "" { + return fmt.Errorf("a2ui: function response call is required") + } + } + return nil +} diff --git a/agent_sdks/go/a2ui/v010/message_test.go b/agent_sdks/go/a2ui/v010/message_test.go new file mode 100644 index 0000000000..602dd5bbaa --- /dev/null +++ b/agent_sdks/go/a2ui/v010/message_test.go @@ -0,0 +1,552 @@ +package v010 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +func TestFlightStatusMessages(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/01_flight-status.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Name string `json:"name"` + Description string `json:"description"` + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + if len(example.Messages) != 3 { + t.Fatalf("got %d messages, want 3", len(example.Messages)) + } + + tests := []struct { + name string + index int + wantField string + }{ + {"CreateSurface", 0, "createSurface"}, + {"UpdateComponents", 1, "updateComponents"}, + {"UpdateDataModel", 2, "updateDataModel"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var msg ServerMessage + if err := json.Unmarshal(example.Messages[tt.index], &msg); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if msg.Version != Version { + t.Fatalf("version = %q, want %q", msg.Version, Version) + } + switch tt.wantField { + case "createSurface": + if msg.CreateSurface == nil { + t.Fatal("CreateSurface is nil") + } + if msg.CreateSurface.SurfaceID != "gallery-flight-status" { + t.Fatalf("surfaceId = %q", msg.CreateSurface.SurfaceID) + } + case "updateComponents": + if msg.UpdateComponents == nil { + t.Fatal("UpdateComponents is nil") + } + if len(msg.UpdateComponents.Components) == 0 { + t.Fatal("no components") + } + case "updateDataModel": + if msg.UpdateDataModel == nil { + t.Fatal("UpdateDataModel is nil") + } + } + + // Round-trip: marshal and compare JSON equivalence. + jsonEquivalent(t, example.Messages[tt.index], msg) + }) + } +} + +func TestServerMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "create_surface", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{ + SurfaceID: "test-1", + CatalogID: "https://example.com/catalog.json", + }, + }, + }, + { + name: "delete_surface", + msg: ServerMessage{ + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "test-1"}, + }, + }, + { + name: "update_data_model", + msg: ServerMessage{ + Version: Version, + UpdateDataModel: &UpdateDataModel{ + SurfaceID: "test-1", + Path: "/count", + Value: float64(42), + }, + }, + }, + { + name: "call_function", + msg: ServerMessage{ + Version: Version, + FunctionCallID: "call-1", + WantResponse: true, + CallFunction: &FunctionCall{ + Call: "lookup", + ReturnType: ReturnTypeString, + }, + }, + }, + { + name: "action_response", + msg: ServerMessage{ + Version: Version, + ActionID: "action-1", + ActionResponse: ptr(ActionResponseValue("done")), + }, + }, + { + name: "action_response_null", + msg: ServerMessage{ + Version: Version, + ActionID: "action-1", + ActionResponse: ptr(ActionResponseValue(nil)), + }, + }, + { + name: "action_response_error", + msg: ServerMessage{ + Version: Version, + ActionID: "action-1", + ActionResponse: &ActionResponse{ + Error: &ActionResponseError{Code: "FAILED", Message: "failed"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestServerMessageCallFunctionWantResponseRoundTrip(t *testing.T) { + data := []byte(`{"version":"v0.10","functionCallId":"call-1","wantResponse":true,"callFunction":{"call":"lookup","returnType":"string"}}`) + var msg ServerMessage + if err := json.Unmarshal(data, &msg); err != nil { + t.Fatal(err) + } + if !msg.WantResponse { + t.Fatal("WantResponse = false, want true") + } + jsonEquivalent(t, data, msg) +} + +func TestActionResponseRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + response ActionResponse + }{ + {"none", ActionResponse{}}, + {"both", ActionResponse{Value: "done", Error: &ActionResponseError{Code: "ERR", Message: "bad"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.response); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{}`, + `{"value":"done","error":{"code":"ERR","message":"bad"}}`, + } { + var response ActionResponse + if err := json.Unmarshal([]byte(raw), &response); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestClientMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "action", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{ + Name: "submit", + SurfaceID: "test-1", + SourceComponentID: "btn-1", + Timestamp: "2025-01-01T00:00:00Z", + Context: map[string]any{"key": "value"}, + WantResponse: true, + ActionID: "action-1", + }, + }, + }, + { + name: "function_response", + msg: ClientMessage{ + Version: Version, + FunctionResponse: &FunctionResponse{ + FunctionCallID: "call-1", + Call: "lookup", + Value: "ok", + }, + }, + }, + { + name: "error", + msg: ClientMessage{ + Version: Version, + Error: &ClientError{ + Code: "INVALID_FUNCTION", + FunctionCallID: "call-1", + Message: "function not found", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ClientMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestClientErrorRequiresExactlyOneTarget(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "neither", + msg: ClientMessage{ + Version: Version, + Error: &ClientError{Code: "ERR", Message: "bad"}, + }, + }, + { + name: "both", + msg: ClientMessage{ + Version: Version, + Error: &ClientError{Code: "ERR", Message: "bad", SurfaceID: "s1", FunctionCallID: "call-1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.10","error":{"code":"ERR","message":"bad"}}`, + `{"version":"v0.10","error":{"code":"ERR","message":"bad","surfaceId":"s1","functionCallId":"call-1"}}`, + } { + var msg ClientMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestServerMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "none", + msg: ServerMessage{Version: Version}, + }, + { + name: "multiple", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.10"}`, + `{"version":"v0.10","createSurface":{"surfaceId":"s1","catalogId":"cat"},"deleteSurface":{"surfaceId":"s1"}}`, + } { + var msg ServerMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestServerMessageRequiresPayloadIDs(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "call_function_missing_id", + msg: ServerMessage{ + Version: Version, + CallFunction: &FunctionCall{ + Call: "lookup", + ReturnType: ReturnTypeString, + }, + }, + }, + { + name: "call_function_missing_call", + msg: ServerMessage{ + Version: Version, + FunctionCallID: "call-1", + CallFunction: &FunctionCall{ + ReturnType: ReturnTypeString, + }, + }, + }, + { + name: "call_function_missing_return_type", + msg: ServerMessage{ + Version: Version, + FunctionCallID: "call-1", + CallFunction: &FunctionCall{ + Call: "lookup", + }, + }, + }, + { + name: "action_response_missing_id", + msg: ServerMessage{ + Version: Version, + ActionResponse: ptr(ActionResponseValue("done")), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.10","callFunction":{"call":"lookup","returnType":"string"}}`, + `{"version":"v0.10","functionCallId":"call-1","callFunction":{"returnType":"string"}}`, + `{"version":"v0.10","functionCallId":"call-1","callFunction":{"call":"lookup"}}`, + `{"version":"v0.10","actionResponse":{"value":"done"}}`, + } { + var msg ServerMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestClientMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "none", + msg: ClientMessage{Version: Version}, + }, + { + name: "multiple", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{Name: "submit", SurfaceID: "s1", SourceComponentID: "btn", Timestamp: "2025-01-01T00:00:00Z"}, + Error: &ClientError{Code: "ERR", SurfaceID: "s1", Message: "bad"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.10"}`, + `{"version":"v0.10","action":{"name":"submit","surfaceId":"s1","sourceComponentId":"btn","timestamp":"2025-01-01T00:00:00Z"},"error":{"code":"ERR","surfaceId":"s1","message":"bad"}}`, + } { + var msg ClientMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestFunctionResponseRequiresIDAndCall(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "missing_id", + msg: ClientMessage{ + Version: Version, + FunctionResponse: &FunctionResponse{Call: "lookup", Value: "ok"}, + }, + }, + { + name: "missing_call", + msg: ClientMessage{ + Version: Version, + FunctionResponse: &FunctionResponse{FunctionCallID: "call-1", Value: "ok"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.10","functionResponse":{"call":"lookup","value":"ok"}}`, + `{"version":"v0.10","functionResponse":{"functionCallId":"call-1","value":"ok"}}`, + } { + var msg ClientMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestServerMessageListWrapper(t *testing.T) { + wrapper := ServerMessageListWrapper{ + Messages: []ServerMessage{ + { + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + }, + { + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + data, err := json.Marshal(wrapper) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessageListWrapper + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, wrapper) { + t.Fatalf("round-trip mismatch\n got: %+v\n want: %+v", got, wrapper) + } +} + +func TestClientMessageListWrapperEmpty(t *testing.T) { + data := []byte(`{"messages":[]}`) + var w ClientMessageListWrapper + if err := json.Unmarshal(data, &w); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(w.Messages) != 0 { + t.Fatalf("got %d messages, want 0", len(w.Messages)) + } +} + +// jsonEquivalent marshals v and checks that the result is semantically +// equivalent to the original JSON. Empty maps/objects and missing fields +// are treated as equivalent (omitempty normalization). +func jsonEquivalent(t *testing.T, original json.RawMessage, v any) { + t.Helper() + remarshaled, err := json.Marshal(v) + if err != nil { + t.Fatalf("re-marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("unmarshal re-marshaled: %v", err) + } + if err := json.Unmarshal(original, &want); err != nil { + t.Fatalf("unmarshal original: %v", err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Fatalf("JSON not equivalent:\n got: %s\n want: %s", remarshaled, original) + } +} + +// normalizeJSON removes empty maps and nil values in-place so that +// omitempty differences don't cause false mismatches. +func normalizeJSON(v any) { + switch v := v.(type) { + case map[string]any: + for k, val := range v { + normalizeJSON(val) + // Remove keys whose value is an empty map (matches omitempty behavior). + if m, ok := val.(map[string]any); ok && len(m) == 0 { + delete(v, k) + } + } + case []any: + for _, elem := range v { + normalizeJSON(elem) + } + } +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/01_flight-status.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/01_flight-status.json new file mode 100644 index 0000000000..0242cc3052 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/01_flight-status.json @@ -0,0 +1,201 @@ +{ + "name": "Flight Status", + "description": "Example of flight status demonstrating date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-flight-status", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-flight-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "route-row", "divider", "times-row"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-left", "date"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "header-left", + "component": "Row", + "children": ["flight-indicator", "flight-number"], + "align": "center" + }, + { + "id": "flight-indicator", + "component": "Icon", + "name": "send" + }, + { + "id": "flight-number", + "component": "Text", + "text": { + "path": "/flightNumber" + }, + "variant": "h3" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "E, MMM d" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "route-row", + "component": "Row", + "children": ["origin", "arrow", "destination"], + "align": "center" + }, + { + "id": "origin", + "component": "Text", + "text": { + "path": "/origin" + }, + "variant": "h2" + }, + { + "id": "arrow", + "component": "Text", + "text": "\u2192", + "variant": "h2" + }, + { + "id": "destination", + "component": "Text", + "text": { + "path": "/destination" + }, + "variant": "h2" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "times-row", + "component": "Row", + "children": ["departure-col", "status-col", "arrival-col"], + "justify": "spaceBetween" + }, + { + "id": "departure-col", + "component": "Column", + "children": ["departure-label", "departure-time"], + "align": "start" + }, + { + "id": "departure-label", + "component": "Text", + "text": "Departs", + "variant": "caption" + }, + { + "id": "departure-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/departureTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "status-col", + "component": "Column", + "children": ["status-label", "status-value"], + "align": "center" + }, + { + "id": "status-label", + "component": "Text", + "text": "Status", + "variant": "caption" + }, + { + "id": "status-value", + "component": "Text", + "text": { + "path": "/status" + }, + "variant": "body" + }, + { + "id": "arrival-col", + "component": "Column", + "children": ["arrival-label", "arrival-time"], + "align": "end" + }, + { + "id": "arrival-label", + "component": "Text", + "text": "Arrives", + "variant": "caption" + }, + { + "id": "arrival-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/arrivalTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-flight-status", + "value": { + "flightNumber": "OS 87", + "date": "2025-12-15", + "origin": "Vienna", + "destination": "New York", + "departureTime": "2025-12-15T10:15:00Z", + "status": "On Time", + "arrivalTime": "2025-12-15T14:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/02_email-compose.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/02_email-compose.json new file mode 100644 index 0000000000..60338c85e6 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/02_email-compose.json @@ -0,0 +1,185 @@ +{ + "name": "Email Compose", + "description": "Example of email compose", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-email-compose", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-email-compose", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["from-row", "to-row", "subject-row", "divider", "message", "actions"] + }, + { + "id": "from-row", + "component": "Row", + "children": ["from-label", "from-value"], + "align": "center" + }, + { + "id": "from-label", + "component": "Text", + "text": "FROM", + "variant": "caption" + }, + { + "id": "from-value", + "component": "Text", + "text": { + "path": "/from" + }, + "variant": "body" + }, + { + "id": "to-row", + "component": "Row", + "children": ["to-label", "to-value"], + "align": "center" + }, + { + "id": "to-label", + "component": "Text", + "text": "TO", + "variant": "caption" + }, + { + "id": "to-value", + "component": "Text", + "text": { + "path": "/to" + }, + "variant": "body" + }, + { + "id": "subject-row", + "component": "Row", + "children": ["subject-label", "subject-value"], + "align": "center" + }, + { + "id": "subject-label", + "component": "Text", + "text": "SUBJECT", + "variant": "caption" + }, + { + "id": "subject-value", + "component": "Text", + "text": { + "path": "/subject" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "message", + "component": "Column", + "children": ["greeting", "body-text", "closing", "signature"] + }, + { + "id": "greeting", + "component": "Text", + "text": { + "path": "/greeting" + }, + "variant": "body" + }, + { + "id": "body-text", + "component": "Text", + "text": { + "path": "/body" + }, + "variant": "body" + }, + { + "id": "closing", + "component": "Text", + "text": { + "path": "/closing" + }, + "variant": "body" + }, + { + "id": "signature", + "component": "Text", + "text": { + "path": "/signature" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["send-btn", "discard-btn"] + }, + { + "id": "send-btn-text", + "component": "Text", + "text": "Send email" + }, + { + "id": "send-btn", + "component": "Button", + "child": "send-btn-text", + "action": { + "event": { + "name": "send", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-email-compose", + "value": { + "from": "alex@acme.com", + "to": "jordan@acme.com", + "subject": "Q4 Revenue Forecast", + "greeting": "Hi Jordan,", + "body": "Following up on our call. Please review the attached Q4 forecast and let me know if you have questions before the board meeting.", + "closing": "Best,", + "signature": "Alex" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/03_calendar-day.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/03_calendar-day.json new file mode 100644 index 0000000000..a7bd098819 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/03_calendar-day.json @@ -0,0 +1,166 @@ +{ + "name": "Calendar Day", + "description": "Example of calendar day demonstrating dynamic templating, relative paths, and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-calendar-day", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-calendar-day", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "divider", "actions"] + }, + { + "id": "header-row", + "component": "Row", + "children": ["date-col", "events-col"] + }, + { + "id": "date-col", + "component": "Column", + "children": ["day-name", "day-number"], + "align": "start" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-number", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "d" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "events-col", + "component": "Column", + "children": { + "path": "/events", + "componentId": "event-template" + } + }, + { + "id": "event-template", + "component": "Column", + "children": ["event-title", "event-time"] + }, + { + "id": "event-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "event-time", + "component": "Text", + "text": { + "path": "time" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-btn", "discard-btn"] + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to calendar" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-calendar-day", + "value": { + "date": "2025-12-28", + "events": [ + { + "title": "Lunch", + "time": "12:00 - 12:45 PM" + }, + { + "title": "Q1 roadmap review", + "time": "1:00 - 2:00 PM" + }, + { + "title": "Team standup", + "time": "3:30 - 4:00 PM" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/04_weather-current.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/04_weather-current.json new file mode 100644 index 0000000000..4d14ed30a3 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/04_weather-current.json @@ -0,0 +1,168 @@ +{ + "name": "Weather Current", + "description": "Example of weather current demonstrating templating and string formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-weather-current", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-weather-current", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["temp-row", "location", "description", "forecast-row"], + "align": "center" + }, + { + "id": "temp-row", + "component": "Row", + "children": ["temp-high", "temp-low"], + "align": "start" + }, + { + "id": "temp-high", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempHigh}°" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "temp-low", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempLow}°" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "location", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "caption" + }, + { + "id": "forecast-row", + "component": "Row", + "children": { + "path": "/forecast", + "componentId": "forecast-day-template" + }, + "justify": "spaceAround" + }, + { + "id": "forecast-day-template", + "component": "Column", + "children": ["day-name", "day-icon", "day-temp"], + "align": "center" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "date" + }, + "format": "E" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-icon", + "component": "Text", + "text": { + "path": "icon" + }, + "variant": "h3" + }, + { + "id": "day-temp", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${temp}°" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-weather-current", + "value": { + "tempHigh": 72, + "tempLow": 58, + "location": "Austin, TX", + "description": "Clear skies with light breeze", + "forecast": [ + { + "date": "2025-12-16", + "icon": "☀️", + "temp": 74 + }, + { + "date": "2025-12-17", + "icon": "☀️", + "temp": 76 + }, + { + "date": "2025-12-18", + "icon": "⛅", + "temp": 71 + }, + { + "date": "2025-12-19", + "icon": "☀️", + "temp": 73 + }, + { + "date": "2025-12-20", + "icon": "☀️", + "temp": 75 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/05_product-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/05_product-card.json new file mode 100644 index 0000000000..98c4f9762f --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/05_product-card.json @@ -0,0 +1,151 @@ +{ + "name": "Product Card", + "description": "Example of product card demonstrating currency formatting and pluralization.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-product-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-product-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["image", "details"] + }, + { + "id": "image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "details", + "component": "Column", + "children": ["name", "rating-row", "price-row", "actions"] + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["stars", "reviews"], + "align": "center" + }, + { + "id": "stars", + "component": "Text", + "text": { + "path": "/stars" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "price-row", + "component": "Row", + "children": ["price", "original-price"], + "align": "start" + }, + { + "id": "price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "original-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/originalPrice" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-cart-btn"] + }, + { + "id": "add-cart-btn-text", + "component": "Text", + "text": "Add to Cart" + }, + { + "id": "add-cart-btn", + "component": "Button", + "child": "add-cart-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "addToCart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-product-card", + "value": { + "imageUrl": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&h=200&fit=crop", + "name": "Wireless Headphones Pro", + "stars": "★★★★★", + "reviewCount": 2847, + "price": 199.99, + "originalPrice": 249.99 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/06_music-player.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/06_music-player.json new file mode 100644 index 0000000000..c83d3843e7 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/06_music-player.json @@ -0,0 +1,165 @@ +{ + "name": "Music Player", + "description": "Example of music player", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-music-player", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-music-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["album-art", "track-info", "progress", "time-row", "controls"], + "align": "center" + }, + { + "id": "album-art", + "component": "Image", + "url": { + "path": "/albumArt" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["song-title", "artist"], + "align": "center" + }, + { + "id": "song-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "artist", + "component": "Text", + "text": { + "path": "/artist" + }, + "variant": "caption" + }, + { + "id": "progress", + "component": "Slider", + "value": { + "path": "/progress" + }, + "max": 1 + }, + { + "id": "time-row", + "component": "Row", + "children": ["current-time", "total-time"], + "justify": "spaceBetween" + }, + { + "id": "current-time", + "component": "Text", + "text": { + "path": "/currentTime" + }, + "variant": "caption" + }, + { + "id": "total-time", + "component": "Text", + "text": { + "path": "/totalTime" + }, + "variant": "caption" + }, + { + "id": "controls", + "component": "Row", + "children": ["prev-btn", "play-btn", "next-btn"], + "justify": "center" + }, + { + "id": "prev-btn-icon", + "component": "Icon", + "name": "skipPrevious" + }, + { + "id": "prev-btn", + "component": "Button", + "child": "prev-btn-icon", + "action": { + "event": { + "name": "previous", + "context": {} + } + } + }, + { + "id": "play-btn-icon", + "component": "Icon", + "name": { + "path": "/playIcon" + } + }, + { + "id": "play-btn", + "component": "Button", + "child": "play-btn-icon", + "action": { + "event": { + "name": "playPause", + "context": {} + } + } + }, + { + "id": "next-btn-icon", + "component": "Icon", + "name": "skipNext" + }, + { + "id": "next-btn", + "component": "Button", + "child": "next-btn-icon", + "action": { + "event": { + "name": "next", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-music-player", + "value": { + "albumArt": "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=300&h=300&fit=crop", + "title": "Blinding Lights", + "artist": "The Weeknd", + "album": "After Hours", + "progress": 0.45, + "currentTime": "1:48", + "totalTime": "4:22", + "playIcon": "pause" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/07_task-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/07_task-card.json new file mode 100644 index 0000000000..37296e01ba --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/07_task-card.json @@ -0,0 +1,107 @@ +{ + "name": "Task Card", + "description": "Example of task card demonstrating CheckBox and DateTimeInput.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-task-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-task-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["status-checkbox", "content", "priority"], + "align": "start" + }, + { + "id": "status-checkbox", + "component": "CheckBox", + "label": "", + "value": { + "path": "/completed" + } + }, + { + "id": "content", + "component": "Column", + "children": ["title", "description", "meta-row"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["due-date-input", "project"], + "align": "center" + }, + { + "id": "due-date-input", + "component": "DateTimeInput", + "label": "Due", + "value": { + "path": "/dueDate" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "project", + "component": "Text", + "text": { + "path": "/project" + }, + "variant": "caption" + }, + { + "id": "priority", + "component": "Icon", + "name": { + "path": "/priorityIcon" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-task-card", + "value": { + "completed": false, + "title": "Review pull request", + "description": "Review and approve the authentication module changes.", + "dueDate": "2025-12-15T17:00:00Z", + "project": "Backend", + "priorityIcon": "priority_high" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/08_user-profile.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/08_user-profile.json new file mode 100644 index 0000000000..8bea67a025 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/08_user-profile.json @@ -0,0 +1,190 @@ +{ + "name": "User Profile", + "description": "Example of user profile demonstrating number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-user-profile", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-user-profile", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "info", "bio", "stats-row", "follow-btn"], + "align": "center" + }, + { + "id": "header", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "info", + "component": "Column", + "children": ["name", "username"], + "align": "center" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "username", + "component": "Text", + "text": { + "path": "/username" + }, + "variant": "caption" + }, + { + "id": "bio", + "component": "Text", + "text": { + "path": "/bio" + }, + "variant": "body" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["followers-col", "following-col", "posts-col"], + "justify": "spaceAround" + }, + { + "id": "followers-col", + "component": "Column", + "children": ["followers-count", "followers-label"], + "align": "center" + }, + { + "id": "followers-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/followers" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "followers-label", + "component": "Text", + "text": "Followers", + "variant": "caption" + }, + { + "id": "following-col", + "component": "Column", + "children": ["following-count", "following-label"], + "align": "center" + }, + { + "id": "following-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/following" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "following-label", + "component": "Text", + "text": "Following", + "variant": "caption" + }, + { + "id": "posts-col", + "component": "Column", + "children": ["posts-count", "posts-label"], + "align": "center" + }, + { + "id": "posts-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/posts" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "posts-label", + "component": "Text", + "text": "Posts", + "variant": "caption" + }, + { + "id": "follow-btn-text", + "component": "Text", + "text": { + "path": "/followText" + } + }, + { + "id": "follow-btn", + "component": "Button", + "child": "follow-btn-text", + "action": { + "event": { + "name": "follow", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-user-profile", + "value": { + "avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop", + "name": "Sarah Chen", + "username": "@sarahchen", + "bio": "Product Designer at Tech Co. Creating delightful experiences.", + "followers": 12400, + "following": 892, + "posts": 347, + "followText": "Follow" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/09_login-form.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/09_login-form.json new file mode 100644 index 0000000000..2edaa23453 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/09_login-form.json @@ -0,0 +1,214 @@ +{ + "name": "Login Form with Validation", + "description": "Example of login form demonstrating validation checks and logic.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-login-form", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-login-form", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header", + "email-field", + "password-field", + "login-btn", + "divider", + "signup-text" + ] + }, + { + "id": "header", + "component": "Column", + "children": ["title", "subtitle"], + "align": "center" + }, + { + "id": "title", + "component": "Text", + "text": "Welcome back", + "variant": "h2" + }, + { + "id": "subtitle", + "component": "Text", + "text": "Sign in to your account", + "variant": "caption" + }, + { + "id": "email-field", + "component": "TextField", + "value": { + "path": "/email" + }, + "label": "Email", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Email is required" + }, + { + "condition": { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Please enter a valid email address" + } + ] + }, + { + "id": "password-field", + "component": "TextField", + "value": { + "path": "/password" + }, + "label": "Password", + "variant": "obscured", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/password" + } + } + }, + "message": "Password is required" + }, + { + "condition": { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + }, + "message": "Password must be at least 8 characters long" + } + ] + }, + { + "id": "login-btn-text", + "component": "Text", + "text": "Sign in" + }, + { + "id": "login-btn", + "component": "Button", + "child": "login-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + } + ] + } + }, + "message": "Please fix errors before signing in" + } + ], + "action": { + "event": { + "name": "login", + "context": { + "email": { + "path": "/email" + } + } + } + } + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "signup-text", + "component": "Row", + "children": ["no-account", "signup-link"], + "justify": "center" + }, + { + "id": "no-account", + "component": "Text", + "text": "Don't have an account?", + "variant": "caption" + }, + { + "id": "signup-link-text", + "component": "Text", + "text": "Sign up" + }, + { + "id": "signup-link", + "component": "Button", + "child": "signup-link-text", + "action": { + "event": { + "name": "signup", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-login-form", + "value": { + "email": "", + "password": "" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/10_notification-permission.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/10_notification-permission.json new file mode 100644 index 0000000000..0eb9b1d468 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/10_notification-permission.json @@ -0,0 +1,105 @@ +{ + "name": "Notification Permission", + "description": "Example of notification permission", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-notification-permission", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-notification-permission", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["icon", "title", "description", "actions"], + "align": "center" + }, + { + "id": "icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["yes-btn", "no-btn"], + "justify": "center" + }, + { + "id": "yes-btn-text", + "component": "Text", + "text": "Yes" + }, + { + "id": "yes-btn", + "component": "Button", + "child": "yes-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "no-btn-text", + "component": "Text", + "text": "No" + }, + { + "id": "no-btn", + "component": "Button", + "child": "no-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-notification-permission", + "value": { + "icon": "check", + "title": "Enable notification", + "description": "Get alerts for order status changes" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/11_purchase-complete.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/11_purchase-complete.json new file mode 100644 index 0000000000..c9ecce6e3e --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/11_purchase-complete.json @@ -0,0 +1,169 @@ +{ + "name": "Purchase Complete", + "description": "Example of purchase complete demonstrating currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-purchase-complete", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-purchase-complete", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "success-icon", + "title", + "product-row", + "divider", + "details-col", + "view-btn" + ], + "align": "center" + }, + { + "id": "success-icon", + "component": "Icon", + "name": "check" + }, + { + "id": "title", + "component": "Text", + "text": "Purchase Complete", + "variant": "h2" + }, + { + "id": "product-row", + "component": "Row", + "children": ["product-image", "product-info"], + "align": "center" + }, + { + "id": "product-image", + "component": "Image", + "url": { + "path": "/productImage" + }, + "fit": "cover" + }, + { + "id": "product-info", + "component": "Column", + "children": ["product-name", "product-price"] + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h4" + }, + { + "id": "product-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "details-col", + "component": "Column", + "children": ["delivery-row", "seller-row"] + }, + { + "id": "delivery-row", + "component": "Row", + "children": ["delivery-icon", "delivery-text"], + "align": "center" + }, + { + "id": "delivery-icon", + "component": "Icon", + "name": "arrowForward" + }, + { + "id": "delivery-text", + "component": "Text", + "text": { + "path": "/deliveryDate" + }, + "variant": "body" + }, + { + "id": "seller-row", + "component": "Row", + "children": ["seller-label", "seller-name"] + }, + { + "id": "seller-label", + "component": "Text", + "text": "Sold by:", + "variant": "caption" + }, + { + "id": "seller-name", + "component": "Text", + "text": { + "path": "/seller" + }, + "variant": "body" + }, + { + "id": "view-btn-text", + "component": "Text", + "text": "View Order Details" + }, + { + "id": "view-btn", + "component": "Button", + "child": "view-btn-text", + "action": { + "event": { + "name": "view_details", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-purchase-complete", + "value": { + "productImage": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&h=100&fit=crop", + "productName": "Wireless Headphones Pro", + "price": 199.99, + "deliveryDate": "Arrives Dec 18 - Dec 20", + "seller": "TechStore Official" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/12_chat-message.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/12_chat-message.json new file mode 100644 index 0000000000..f517eb1c87 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/12_chat-message.json @@ -0,0 +1,144 @@ +{ + "name": "Chat Message", + "description": "Example of chat message demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-chat-message", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-chat-message", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "messages-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["channel-icon", "channel-name"], + "align": "center" + }, + { + "id": "channel-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "channel-name", + "component": "Text", + "text": { + "path": "/channelName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "messages-list", + "component": "Column", + "children": { + "path": "/messages", + "componentId": "message-template" + }, + "align": "start" + }, + { + "id": "message-template", + "component": "Row", + "children": ["msg-avatar", "msg-content"], + "align": "start" + }, + { + "id": "msg-avatar", + "component": "Image", + "url": { + "path": "avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "msg-content", + "component": "Column", + "children": ["msg-header", "msg-text"] + }, + { + "id": "msg-header", + "component": "Row", + "children": ["msg-username", "msg-time"], + "align": "center" + }, + { + "id": "msg-username", + "component": "Text", + "text": { + "path": "username" + }, + "variant": "h4" + }, + { + "id": "msg-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "timestamp" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "msg-text", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-chat-message", + "value": { + "channelName": "project-updates", + "messages": [ + { + "avatar": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop", + "username": "Mike Chen", + "timestamp": "2025-12-15T10:32:00Z", + "text": "Just pushed the new API changes. Ready for review." + }, + { + "avatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=40&h=40&fit=crop", + "username": "Sarah Kim", + "timestamp": "2025-12-15T10:45:00Z", + "text": "Great! I'll take a look after standup." + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/13_coffee-order.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/13_coffee-order.json new file mode 100644 index 0000000000..b92dcadb29 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/13_coffee-order.json @@ -0,0 +1,253 @@ +{ + "name": "Coffee Order", + "description": "Example of coffee order demonstrating templating and currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-coffee-order", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-coffee-order", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "items-list", "divider", "totals", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["coffee-icon", "store-name"], + "align": "center" + }, + { + "id": "coffee-icon", + "component": "Icon", + "name": "favorite" + }, + { + "id": "store-name", + "component": "Text", + "text": { + "path": "/storeName" + }, + "variant": "h3" + }, + { + "id": "items-list", + "component": "Column", + "children": { + "path": "/items", + "componentId": "order-item-template" + } + }, + { + "id": "order-item-template", + "component": "Row", + "children": ["item-details", "item-price"], + "justify": "spaceBetween", + "align": "start" + }, + { + "id": "item-details", + "component": "Column", + "children": ["item-name", "item-size"] + }, + { + "id": "item-name", + "component": "Text", + "text": { + "path": "name" + }, + "variant": "body" + }, + { + "id": "item-size", + "component": "Text", + "text": { + "path": "size" + }, + "variant": "caption" + }, + { + "id": "item-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "totals", + "component": "Column", + "children": ["subtotal-row", "tax-row", "total-row"] + }, + { + "id": "subtotal-row", + "component": "Row", + "children": ["subtotal-label", "subtotal-value"], + "justify": "spaceBetween" + }, + { + "id": "subtotal-label", + "component": "Text", + "text": "Subtotal", + "variant": "caption" + }, + { + "id": "subtotal-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/subtotal" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "tax-row", + "component": "Row", + "children": ["tax-label", "tax-value"], + "justify": "spaceBetween" + }, + { + "id": "tax-label", + "component": "Text", + "text": "Tax", + "variant": "caption" + }, + { + "id": "tax-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/tax" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/total" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h4" + }, + { + "id": "actions", + "component": "Row", + "children": ["purchase-btn", "add-btn"] + }, + { + "id": "purchase-btn-text", + "component": "Text", + "text": "Purchase" + }, + { + "id": "purchase-btn", + "component": "Button", + "child": "purchase-btn-text", + "action": { + "event": { + "name": "purchase", + "context": {} + } + } + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to cart" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add_to_cart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-coffee-order", + "value": { + "storeName": "Sunrise Coffee", + "items": [ + { + "name": "Oat Milk Latte", + "size": "Grande, Extra Shot", + "price": 6.45 + }, + { + "name": "Chocolate Croissant", + "size": "Warmed", + "price": 4.25 + } + ], + "subtotal": 10.7, + "tax": 0.96, + "total": 11.66 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/14_sports-player.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/14_sports-player.json new file mode 100644 index 0000000000..a1fe09b17c --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/14_sports-player.json @@ -0,0 +1,177 @@ +{ + "name": "Sports Player", + "description": "Example of sports player", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-sports-player", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-sports-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["player-image", "player-info", "divider", "stats-row"], + "align": "center" + }, + { + "id": "player-image", + "component": "Image", + "url": { + "path": "/playerImage" + }, + "fit": "cover" + }, + { + "id": "player-info", + "component": "Column", + "children": ["player-name", "player-details"], + "align": "center" + }, + { + "id": "player-name", + "component": "Text", + "text": { + "path": "/playerName" + }, + "variant": "h2" + }, + { + "id": "player-details", + "component": "Row", + "children": ["player-number", "player-team"], + "align": "center" + }, + { + "id": "player-number", + "component": "Text", + "text": { + "path": "/number" + }, + "variant": "h3" + }, + { + "id": "player-team", + "component": "Text", + "text": { + "path": "/team" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["stat1", "stat2", "stat3"], + "justify": "spaceAround" + }, + { + "id": "stat1", + "component": "Column", + "children": ["stat1-value", "stat1-label"], + "align": "center" + }, + { + "id": "stat1-value", + "component": "Text", + "text": { + "path": "/stat1/value" + }, + "variant": "h3" + }, + { + "id": "stat1-label", + "component": "Text", + "text": { + "path": "/stat1/label" + }, + "variant": "caption" + }, + { + "id": "stat2", + "component": "Column", + "children": ["stat2-value", "stat2-label"], + "align": "center" + }, + { + "id": "stat2-value", + "component": "Text", + "text": { + "path": "/stat2/value" + }, + "variant": "h3" + }, + { + "id": "stat2-label", + "component": "Text", + "text": { + "path": "/stat2/label" + }, + "variant": "caption" + }, + { + "id": "stat3", + "component": "Column", + "children": ["stat3-value", "stat3-label"], + "align": "center" + }, + { + "id": "stat3-value", + "component": "Text", + "text": { + "path": "/stat3/value" + }, + "variant": "h3" + }, + { + "id": "stat3-label", + "component": "Text", + "text": { + "path": "/stat3/label" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-sports-player", + "value": { + "playerImage": "https://images.unsplash.com/photo-1546519638-68e109498ffc?w=200&h=200&fit=crop", + "playerName": "Marcus Johnson", + "number": "#23", + "team": "LA Lakers", + "stat1": { + "value": "28.4", + "label": "PPG" + }, + "stat2": { + "value": "7.2", + "label": "RPG" + }, + "stat3": { + "value": "6.8", + "label": "APG" + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/15_account-balance.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/15_account-balance.json new file mode 100644 index 0000000000..b22b24b0b5 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/15_account-balance.json @@ -0,0 +1,126 @@ +{ + "name": "Account Balance", + "description": "Example of account balance demonstrating currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-account-balance", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-account-balance", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "balance", "updated", "divider", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["account-icon", "account-name"], + "align": "center" + }, + { + "id": "account-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "account-name", + "component": "Text", + "text": { + "path": "/accountName" + }, + "variant": "h4" + }, + { + "id": "balance", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/balance" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "updated", + "component": "Text", + "text": { + "path": "/lastUpdated" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["transfer-btn", "pay-btn"] + }, + { + "id": "transfer-btn-text", + "component": "Text", + "text": "Transfer" + }, + { + "id": "transfer-btn", + "component": "Button", + "child": "transfer-btn-text", + "action": { + "event": { + "name": "transfer", + "context": {} + } + } + }, + { + "id": "pay-btn-text", + "component": "Text", + "text": "Pay Bill" + }, + { + "id": "pay-btn", + "component": "Button", + "child": "pay-btn-text", + "action": { + "event": { + "name": "pay_bill", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-account-balance", + "value": { + "accountName": "Primary Checking", + "balance": 12458.32, + "lastUpdated": "Updated just now" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/16_workout-summary.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/16_workout-summary.json new file mode 100644 index 0000000000..6db0735295 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/16_workout-summary.json @@ -0,0 +1,160 @@ +{ + "name": "Workout Summary", + "description": "Example of workout summary demonstrating number and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-workout-summary", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-workout-summary", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "metrics-row", "date"] + }, + { + "id": "header", + "component": "Row", + "children": ["workout-icon", "title"], + "align": "center" + }, + { + "id": "workout-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": "Workout Complete", + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "metrics-row", + "component": "Row", + "children": ["duration-col", "calories-col", "distance-col"], + "justify": "spaceAround" + }, + { + "id": "duration-col", + "component": "Column", + "children": ["duration-value", "duration-label"], + "align": "center" + }, + { + "id": "duration-value", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "h3" + }, + { + "id": "duration-label", + "component": "Text", + "text": "Duration", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} km" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE, MMM d 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-workout-summary", + "value": { + "icon": "directions_run", + "workoutType": "Morning Run", + "duration": "32:15", + "calories": 385, + "distance": 5.2, + "date": "2025-12-15T07:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/17_event-detail.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/17_event-detail.json new file mode 100644 index 0000000000..a62fbbb995 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/17_event-detail.json @@ -0,0 +1,144 @@ +{ + "name": "Event Detail", + "description": "Example of event detail demonstrating date and string formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-event-detail", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-event-detail", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title", "time-row", "location-row", "description", "divider", "actions"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h2" + }, + { + "id": "time-row", + "component": "Row", + "children": ["time-icon", "time-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "time-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatDate(value: ${/start}, format: 'E, MMM d')} • ${formatDate(value: ${/start}, format: 'h:mm a')} - ${formatDate(value: ${/end}, format: 'h:mm a')}" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["accept-btn", "decline-btn"] + }, + { + "id": "accept-btn-text", + "component": "Text", + "text": "Accept" + }, + { + "id": "accept-btn", + "component": "Button", + "child": "accept-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "decline-btn-text", + "component": "Text", + "text": "Decline" + }, + { + "id": "decline-btn", + "component": "Button", + "child": "decline-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-event-detail", + "value": { + "title": "Product Launch Meeting", + "start": "2025-12-19T14:00:00Z", + "end": "2025-12-19T15:30:00Z", + "location": "Conference Room A, Building 2", + "description": "Review final product specs and marketing materials before the Q1 launch." + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/18_track-list.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/18_track-list.json new file mode 100644 index 0000000000..19a2d6853a --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/18_track-list.json @@ -0,0 +1,152 @@ +{ + "name": "Track List", + "description": "Example of track list demonstrating templating, relative paths, and number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-track-list", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-track-list", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "tracks-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["playlist-icon", "playlist-name"], + "align": "center" + }, + { + "id": "playlist-icon", + "component": "Icon", + "name": "play" + }, + { + "id": "playlist-name", + "component": "Text", + "text": { + "path": "/playlistName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "tracks-list", + "component": "Column", + "children": { + "path": "/tracks", + "componentId": "track-item-template" + } + }, + { + "id": "track-item-template", + "component": "Row", + "children": ["track-num", "track-art", "track-info", "track-duration"], + "align": "center" + }, + { + "id": "track-num", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "number" + } + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "track-art", + "component": "Image", + "url": { + "path": "art" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["track-title", "track-artist"] + }, + { + "id": "track-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "track-artist", + "component": "Text", + "text": { + "path": "artist" + }, + "variant": "caption" + }, + { + "id": "track-duration", + "component": "Text", + "text": { + "path": "duration" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-track-list", + "value": { + "playlistName": "Focus Flow", + "tracks": [ + { + "number": 1, + "art": "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=50&h=50&fit=crop", + "title": "Weightless", + "artist": "Marconi Union", + "duration": "8:09" + }, + { + "number": 2, + "art": "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=50&h=50&fit=crop", + "title": "Clair de Lune", + "artist": "Debussy", + "duration": "5:12" + }, + { + "number": 3, + "art": "https://images.unsplash.com/photo-1507838153414-b4b713384a76?w=50&h=50&fit=crop", + "title": "Ambient Light", + "artist": "Brian Eno", + "duration": "6:45" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/19_software-purchase.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/19_software-purchase.json new file mode 100644 index 0000000000..2c38be2d63 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/19_software-purchase.json @@ -0,0 +1,194 @@ +{ + "name": "Software Purchase", + "description": "Example of software purchase demonstrating currency formatting and ChoicePicker.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-software-purchase", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-software-purchase", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "title", + "product-name", + "divider1", + "options", + "divider2", + "total-row", + "actions" + ] + }, + { + "id": "title", + "component": "Text", + "text": "Purchase License", + "variant": "h3" + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h2" + }, + { + "id": "divider1", + "component": "Divider" + }, + { + "id": "options", + "component": "Column", + "children": ["seats-row", "period-row"] + }, + { + "id": "seats-row", + "component": "Row", + "children": ["seats-label", "seats-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "seats-label", + "component": "Text", + "text": "Number of seats", + "variant": "body" + }, + { + "id": "seats-value", + "component": "Text", + "text": { + "path": "/seats" + }, + "variant": "h4" + }, + { + "id": "period-row", + "component": "Row", + "children": ["period-label", "period-picker"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "period-label", + "component": "Text", + "text": "Billing period", + "variant": "body" + }, + { + "id": "period-picker", + "component": "ChoicePicker", + "options": [ + { + "label": "Annual", + "value": "annual" + }, + { + "label": "Monthly", + "value": "monthly" + } + ], + "value": { + "path": "/billingPeriod" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "divider2", + "component": "Divider" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatCurrency(value: ${/total}, currency: 'USD')}/year" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "actions", + "component": "Row", + "children": ["confirm-btn", "cancel-btn"] + }, + { + "id": "confirm-btn-text", + "component": "Text", + "text": "Confirm Purchase" + }, + { + "id": "confirm-btn", + "component": "Button", + "child": "confirm-btn-text", + "action": { + "event": { + "name": "confirm", + "context": {} + } + } + }, + { + "id": "cancel-btn-text", + "component": "Text", + "text": "Cancel" + }, + { + "id": "cancel-btn", + "component": "Button", + "child": "cancel-btn-text", + "action": { + "event": { + "name": "cancel", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-software-purchase", + "value": { + "productName": "Design Suite Pro", + "seats": "10 seats", + "billingPeriod": ["annual"], + "total": 1188.0 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/20_restaurant-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/20_restaurant-card.json new file mode 100644 index 0000000000..ea4b78c715 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/20_restaurant-card.json @@ -0,0 +1,140 @@ +{ + "name": "Restaurant Card", + "description": "Example of restaurant card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-restaurant-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-restaurant-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["restaurant-image", "content"] + }, + { + "id": "restaurant-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["name-row", "cuisine", "rating-row", "details-row"] + }, + { + "id": "name-row", + "component": "Row", + "children": ["restaurant-name", "price-range"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "restaurant-name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "price-range", + "component": "Text", + "text": { + "path": "/priceRange" + }, + "variant": "body" + }, + { + "id": "cuisine", + "component": "Text", + "text": { + "path": "/cuisine" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "reviews"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "path": "/reviewCount" + }, + "variant": "caption" + }, + { + "id": "details-row", + "component": "Row", + "children": ["distance", "delivery-time"] + }, + { + "id": "distance", + "component": "Text", + "text": { + "path": "/distance" + }, + "variant": "caption" + }, + { + "id": "delivery-time", + "component": "Text", + "text": { + "path": "/deliveryTime" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-restaurant-card", + "value": { + "image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=300&h=150&fit=crop", + "name": "The Italian Kitchen", + "priceRange": "$$$", + "cuisine": "Italian \u2022 Pasta \u2022 Wine Bar", + "rating": "4.8", + "reviewCount": "(2,847 reviews)", + "distance": "0.8 mi", + "deliveryTime": "25-35 min" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/21_shipping-status.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/21_shipping-status.json new file mode 100644 index 0000000000..afa6b645fd --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/21_shipping-status.json @@ -0,0 +1,137 @@ +{ + "name": "Shipping Status", + "description": "Example of shipping status demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-shipping-status", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-shipping-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "tracking-number", "divider", "steps-list", "eta"] + }, + { + "id": "header", + "component": "Row", + "children": ["package-icon", "title"], + "align": "center" + }, + { + "id": "package-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "title", + "component": "Text", + "text": "Package Status", + "variant": "h3" + }, + { + "id": "tracking-number", + "component": "Text", + "text": { + "path": "/trackingNumber" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "steps-list", + "component": "Column", + "children": { + "path": "/steps", + "componentId": "step-item-template" + } + }, + { + "id": "step-item-template", + "component": "Row", + "children": ["step-icon", "step-text"], + "align": "center" + }, + { + "id": "step-icon", + "component": "Icon", + "name": { + "path": "icon" + } + }, + { + "id": "step-text", + "component": "Text", + "text": { + "path": "label" + }, + "variant": "body" + }, + { + "id": "eta", + "component": "Row", + "children": ["eta-icon", "eta-text"], + "align": "center" + }, + { + "id": "eta-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "eta-text", + "component": "Text", + "text": { + "path": "/eta" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-shipping-status", + "value": { + "trackingNumber": "Tracking: 1Z999AA10123456784", + "steps": [ + { + "icon": "check", + "label": "Order Placed" + }, + { + "icon": "check", + "label": "Shipped" + }, + { + "icon": "send", + "label": "Out for Delivery" + }, + { + "icon": "check", + "label": "Delivered" + } + ], + "eta": "Estimated delivery: Today by 8 PM" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/22_credit-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/22_credit-card.json new file mode 100644 index 0000000000..639506e953 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/22_credit-card.json @@ -0,0 +1,117 @@ +{ + "name": "Credit Card", + "description": "Example of credit card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-credit-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-credit-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["card-type-row", "card-number", "card-details"] + }, + { + "id": "card-type-row", + "component": "Row", + "children": ["card-icon", "card-type"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "card-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "card-type", + "component": "Text", + "text": { + "path": "/cardType" + }, + "variant": "h4" + }, + { + "id": "card-number", + "component": "Text", + "text": { + "path": "/cardNumber" + }, + "variant": "h2" + }, + { + "id": "card-details", + "component": "Row", + "children": ["holder-col", "expiry-col"], + "justify": "spaceBetween" + }, + { + "id": "holder-col", + "component": "Column", + "children": ["holder-label", "holder-name"] + }, + { + "id": "holder-label", + "component": "Text", + "text": "CARD HOLDER", + "variant": "caption" + }, + { + "id": "holder-name", + "component": "Text", + "text": { + "path": "/holderName" + }, + "variant": "body" + }, + { + "id": "expiry-col", + "component": "Column", + "children": ["expiry-label", "expiry-date"], + "align": "end" + }, + { + "id": "expiry-label", + "component": "Text", + "text": "EXPIRES", + "variant": "caption" + }, + { + "id": "expiry-date", + "component": "Text", + "text": { + "path": "/expiryDate" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-credit-card", + "value": { + "cardType": "VISA", + "cardNumber": "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 4242", + "holderName": "SARAH JOHNSON", + "expiryDate": "09/27" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/23_step-counter.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/23_step-counter.json new file mode 100644 index 0000000000..a3cc2a1e3f --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/23_step-counter.json @@ -0,0 +1,149 @@ +{ + "name": "Step Counter", + "description": "Example of step counter demonstrating number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-step-counter", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-step-counter", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "steps-display", "goal-text", "divider", "stats-row"], + "align": "center" + }, + { + "id": "header", + "component": "Row", + "children": ["steps-icon", "title"], + "align": "center" + }, + { + "id": "steps-icon", + "component": "Icon", + "name": "person" + }, + { + "id": "title", + "component": "Text", + "text": "Today's Steps", + "variant": "h4" + }, + { + "id": "steps-display", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/steps" + } + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "goal-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/progress}% of ${formatNumber(value: ${/goal})} goal" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["distance-col", "calories-col"], + "justify": "spaceAround" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} mi" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-step-counter", + "value": { + "steps": 8432, + "goal": 10000, + "progress": 84, + "distance": 3.8, + "calories": 312 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/24_recipe-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/24_recipe-card.json new file mode 100644 index 0000000000..5d19c71508 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/24_recipe-card.json @@ -0,0 +1,204 @@ +{ + "name": "Recipe Card", + "description": "Example of recipe card demonstrating Tabs and pluralization.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-recipe-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-recipe-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "tabs-container" + }, + { + "id": "tabs-container", + "component": "Tabs", + "tabs": [ + { + "title": "Overview", + "child": "overview-col" + }, + { + "title": "Ingredients", + "child": "ingredients-list" + }, + { + "title": "Instructions", + "child": "instructions-list" + } + ] + }, + { + "id": "overview-col", + "component": "Column", + "children": ["recipe-image", "overview-content"] + }, + { + "id": "recipe-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "overview-content", + "component": "Column", + "children": ["title", "rating-row", "times-row", "servings"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "review-count"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "review-count", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "times-row", + "component": "Row", + "children": ["prep-time", "cook-time"] + }, + { + "id": "prep-time", + "component": "Row", + "children": ["prep-icon", "prep-text"], + "align": "center" + }, + { + "id": "prep-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "prep-text", + "component": "Text", + "text": { + "path": "/prepTime" + }, + "variant": "caption" + }, + { + "id": "cook-time", + "component": "Row", + "children": ["cook-icon", "cook-text"], + "align": "center" + }, + { + "id": "cook-icon", + "component": "Icon", + "name": "warning" + }, + { + "id": "cook-text", + "component": "Text", + "text": { + "path": "/cookTime" + }, + "variant": "caption" + }, + { + "id": "servings", + "component": "Text", + "text": { + "path": "/servings" + }, + "variant": "caption" + }, + { + "id": "ingredients-list", + "component": "Column", + "children": { + "path": "/ingredients", + "componentId": "item-template" + } + }, + { + "id": "instructions-list", + "component": "Column", + "children": { + "path": "/instructions", + "componentId": "item-template" + } + }, + { + "id": "item-template", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-recipe-card", + "value": { + "image": "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=180&fit=crop", + "title": "Mediterranean Quinoa Bowl", + "rating": "4.9", + "reviewCount": 1247, + "prepTime": "15 min prep", + "cookTime": "20 min cook", + "servings": "Serves 4", + "ingredients": [ + {"text": "1 cup quinoa"}, + {"text": "2 cups water"}, + {"text": "1 cucumber, diced"}, + {"text": "1 cup cherry tomatoes, halved"} + ], + "instructions": [ + {"text": "1. Rinse quinoa and bring to a boil in water."}, + {"text": "2. Reduce heat and simmer for 15 minutes."}, + {"text": "3. Fluff with a fork and let cool."}, + {"text": "4. Mix with diced vegetables."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/25_contact-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/25_contact-card.json new file mode 100644 index 0000000000..ed00ec6e24 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/25_contact-card.json @@ -0,0 +1,175 @@ +{ + "name": "Contact Card", + "description": "Example of contact card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-contact-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-contact-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["avatar-image", "name", "title", "divider", "contact-info", "actions"], + "align": "center" + }, + { + "id": "avatar-image", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "contact-info", + "component": "Column", + "children": ["phone-row", "email-row", "location-row"] + }, + { + "id": "phone-row", + "component": "Row", + "children": ["phone-icon", "phone-text"], + "align": "center" + }, + { + "id": "phone-icon", + "component": "Icon", + "name": "phone" + }, + { + "id": "phone-text", + "component": "Text", + "text": { + "path": "/phone" + }, + "variant": "body" + }, + { + "id": "email-row", + "component": "Row", + "children": ["email-icon", "email-text"], + "align": "center" + }, + { + "id": "email-icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "email-text", + "component": "Text", + "text": { + "path": "/email" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["call-btn", "message-btn"] + }, + { + "id": "call-btn-text", + "component": "Text", + "text": "Call" + }, + { + "id": "call-btn", + "component": "Button", + "child": "call-btn-text", + "action": { + "event": { + "name": "call", + "context": {} + } + } + }, + { + "id": "message-btn-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-btn", + "component": "Button", + "child": "message-btn-text", + "action": { + "event": { + "name": "message", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-contact-card", + "value": { + "avatar": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop", + "name": "David Park", + "title": "Engineering Manager", + "phone": "+1 (555) 234-5678", + "email": "david.park@company.com", + "location": "San Francisco, CA" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/26_podcast-episode.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/26_podcast-episode.json new file mode 100644 index 0000000000..02c1e7b089 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/26_podcast-episode.json @@ -0,0 +1,123 @@ +{ + "name": "Podcast Episode", + "description": "Example of podcast episode demonstrating AudioPlayer and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-podcast-episode", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-podcast-episode", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["artwork", "content"], + "align": "start" + }, + { + "id": "artwork", + "component": "Image", + "url": { + "path": "/artwork" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["show-name", "episode-title", "meta-row", "description", "audio-player"] + }, + { + "id": "show-name", + "component": "Text", + "text": { + "path": "/showName" + }, + "variant": "caption" + }, + { + "id": "episode-title", + "component": "Text", + "text": { + "path": "/episodeTitle" + }, + "variant": "h4" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["duration", "date"] + }, + { + "id": "duration", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "MMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "audio-player", + "component": "AudioPlayer", + "url": { + "path": "/audioUrl" + }, + "description": { + "path": "/episodeTitle" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-podcast-episode", + "value": { + "artwork": "https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=100&h=100&fit=crop", + "showName": "Tech Talk Daily", + "episodeTitle": "The Future of AI in Product Design", + "duration": "45 min", + "date": "2024-12-15", + "description": "How AI is transforming the way we design and build products.", + "audioUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/27_stats-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/27_stats-card.json new file mode 100644 index 0000000000..aa1c1cdee9 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/27_stats-card.json @@ -0,0 +1,106 @@ +{ + "name": "Stats Card", + "description": "Example of stats card demonstrating currency and number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-stats-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-stats-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "value", "trend-row"] + }, + { + "id": "header", + "component": "Row", + "children": ["metric-icon", "metric-name"], + "align": "center" + }, + { + "id": "metric-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "metric-name", + "component": "Text", + "text": { + "path": "/metricName" + }, + "variant": "caption" + }, + { + "id": "value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/value" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "trend-row", + "component": "Row", + "children": ["trend-icon", "trend-text"], + "align": "center" + }, + { + "id": "trend-icon", + "component": "Icon", + "name": { + "path": "/trendIcon" + } + }, + { + "id": "trend-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "+${/trendPercent}% from last month" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-stats-card", + "value": { + "icon": "trending_up", + "metricName": "Monthly Revenue", + "value": 48294, + "trendIcon": "arrow_upward", + "trendPercent": 12.5 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/28_countdown-timer.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/28_countdown-timer.json new file mode 100644 index 0000000000..5032bba156 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/28_countdown-timer.json @@ -0,0 +1,135 @@ +{ + "name": "Countdown Timer", + "description": "Example of countdown timer demonstrating date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-countdown-timer", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-countdown-timer", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["event-name", "countdown-row", "target-date"], + "align": "center" + }, + { + "id": "event-name", + "component": "Text", + "text": { + "path": "/eventName" + }, + "variant": "h3" + }, + { + "id": "countdown-row", + "component": "Row", + "children": ["days-col", "hours-col", "minutes-col"], + "justify": "spaceAround" + }, + { + "id": "days-col", + "component": "Column", + "children": ["days-value", "days-label"], + "align": "center" + }, + { + "id": "days-value", + "component": "Text", + "text": { + "path": "/days" + }, + "variant": "h1" + }, + { + "id": "days-label", + "component": "Text", + "text": "Days", + "variant": "caption" + }, + { + "id": "hours-col", + "component": "Column", + "children": ["hours-value", "hours-label"], + "align": "center" + }, + { + "id": "hours-value", + "component": "Text", + "text": { + "path": "/hours" + }, + "variant": "h1" + }, + { + "id": "hours-label", + "component": "Text", + "text": "Hours", + "variant": "caption" + }, + { + "id": "minutes-col", + "component": "Column", + "children": ["minutes-value", "minutes-label"], + "align": "center" + }, + { + "id": "minutes-value", + "component": "Text", + "text": { + "path": "/minutes" + }, + "variant": "h1" + }, + { + "id": "minutes-label", + "component": "Text", + "text": "Minutes", + "variant": "caption" + }, + { + "id": "target-date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/targetDate" + }, + "format": "MMMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-countdown-timer", + "value": { + "eventName": "Product Launch", + "days": "14", + "hours": "08", + "minutes": "32", + "targetDate": "2025-01-15" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/29_movie-card.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/29_movie-card.json new file mode 100644 index 0000000000..0d9bdb553c --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/29_movie-card.json @@ -0,0 +1,156 @@ +{ + "name": "Movie Card", + "description": "Example of movie card demonstrating Modal and Video components.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-movie-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-movie-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["poster", "content", "trailer-modal"] + }, + { + "id": "poster", + "component": "Image", + "url": { + "path": "/poster" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["title-row", "genre", "rating-row", "runtime", "watch-trailer-btn"] + }, + { + "id": "title-row", + "component": "Row", + "children": ["movie-title", "year"], + "align": "start" + }, + { + "id": "movie-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "year", + "component": "Text", + "text": { + "path": "/year" + }, + "variant": "caption" + }, + { + "id": "genre", + "component": "Text", + "text": { + "path": "/genre" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating-value"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating-value", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "runtime", + "component": "Row", + "children": ["time-icon", "runtime-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "runtime-text", + "component": "Text", + "text": { + "path": "/runtime" + }, + "variant": "caption" + }, + { + "id": "watch-trailer-btn-text", + "component": "Text", + "text": "Watch Trailer" + }, + { + "id": "watch-trailer-btn", + "component": "Button", + "child": "watch-trailer-btn-text", + "action": { + "event": { + "name": "open_trailer" + } + } + }, + { + "id": "trailer-modal", + "component": "Modal", + "trigger": "watch-trailer-btn", + "content": "trailer-video" + }, + { + "id": "trailer-video", + "component": "Video", + "url": { + "path": "/trailerUrl" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-movie-card", + "value": { + "poster": "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=200&h=300&fit=crop", + "title": "Interstellar", + "year": "(2014)", + "genre": "Sci-Fi • Adventure • Drama", + "rating": "8.7/10", + "runtime": "2h 49min", + "trailerUrl": "https://www.w3schools.com/html/mov_bbb.mp4" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/30_live-invitation-builder.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/30_live-invitation-builder.json new file mode 100644 index 0000000000..7ed5bc9f06 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/30_live-invitation-builder.json @@ -0,0 +1,205 @@ +{ + "name": "Live Invitation Builder", + "description": "Demonstrates reactive two-way binding where editor inputs update a live preview in real-time.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-invitation-builder", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-invitation-builder", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "main-content"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "# Invitation Builder", + "variant": "h1" + }, + { + "id": "main-content", + "component": "Row", + "children": ["editor-col", "preview-col"], + "align": "start" + }, + { + "id": "editor-col", + "component": "Column", + "children": [ + "editor-title", + "event-name-input", + "guest-input", + "date-input", + "location-picker" + ], + "weight": 1, + "align": "stretch" + }, + { + "id": "editor-title", + "component": "Text", + "text": "Customize your invitation", + "variant": "h3" + }, + { + "id": "event-name-input", + "component": "TextField", + "label": "Event Name", + "value": { + "path": "/event/name" + } + }, + { + "id": "guest-input", + "component": "TextField", + "label": "Guest of Honor", + "value": { + "path": "/event/guest" + } + }, + { + "id": "date-input", + "component": "DateTimeInput", + "label": "Event Date & Time", + "value": { + "path": "/event/date" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "location-picker", + "component": "ChoicePicker", + "label": "Location", + "options": [ + {"label": "Grand Ballroom", "value": "ballroom"}, + {"label": "Sunset Terrace", "value": "terrace"}, + {"label": "Garden Pavillion", "value": "garden"} + ], + "value": { + "path": "/event/location" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "preview-col", + "component": "Column", + "children": ["preview-title", "invitation-card"], + "weight": 1, + "align": "center" + }, + { + "id": "preview-title", + "component": "Text", + "text": "Live Preview", + "variant": "caption" + }, + { + "id": "invitation-card", + "component": "Card", + "child": "invitation-content" + }, + { + "id": "invitation-content", + "component": "Column", + "children": [ + "invite-image", + "invite-event-name", + "invite-guest-row", + "invite-date-text", + "invite-location-text" + ], + "align": "center" + }, + { + "id": "invite-image", + "component": "Image", + "url": "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop", + "variant": "mediumFeature" + }, + { + "id": "invite-event-name", + "component": "Text", + "text": { + "path": "/event/name" + }, + "variant": "h2" + }, + { + "id": "invite-guest-row", + "component": "Row", + "children": ["invite-for-text", "invite-guest-name"], + "align": "center" + }, + { + "id": "invite-for-text", + "component": "Text", + "text": "Celebrating", + "variant": "body" + }, + { + "id": "invite-guest-name", + "component": "Text", + "text": { + "path": "/event/guest" + }, + "variant": "h3" + }, + { + "id": "invite-date-text", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/event/date" + }, + "format": "EEEE, MMMM d, yyyy 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "invite-location-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Location: ${/event/location/0}" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-invitation-builder", + "value": { + "event": { + "name": "Summer Gala", + "guest": "Alex Johnson", + "date": "2025-07-15T19:00:00Z", + "location": ["terrace"] + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/31_incremental-dashboard.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/31_incremental-dashboard.json new file mode 100644 index 0000000000..c05ed4f312 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/31_incremental-dashboard.json @@ -0,0 +1,128 @@ +{ + "name": "Incremental Dashboard", + "description": "Demonstrates structural evolution of a UI where loading placeholders are incrementally replaced by actual components.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-incremental-dashboard", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json" + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "content-grid"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "System Dashboard", + "variant": "h2" + }, + { + "id": "content-grid", + "component": "Row", + "children": ["left-panel", "right-panel"] + }, + { + "id": "left-panel", + "component": "Column", + "children": ["panel-a-loading"], + "weight": 1 + }, + { + "id": "right-panel", + "component": "Column", + "children": ["panel-b-loading"], + "weight": 1 + }, + { + "id": "panel-a-loading", + "component": "Text", + "text": "Loading analytics...", + "variant": "caption" + }, + { + "id": "panel-b-loading", + "component": "Text", + "text": "Loading logs...", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "left-panel", + "component": "Column", + "children": ["analytics-card"], + "weight": 1 + }, + { + "id": "analytics-card", + "component": "Card", + "child": "analytics-text" + }, + { + "id": "analytics-text", + "component": "Text", + "text": "Analytics are ready.", + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "right-panel", + "component": "Column", + "children": ["logs-list"], + "weight": 1 + }, + { + "id": "logs-list", + "component": "List", + "children": { + "path": "/logs", + "componentId": "log-template" + } + }, + { + "id": "log-template", + "component": "Text", + "text": {"path": "message"}, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-incremental-dashboard", + "value": { + "logs": [ + {"message": "System boot complete."}, + {"message": "All services healthy."}, + {"message": "Waiting for user input."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/32_advanced-form-validator.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/32_advanced-form-validator.json new file mode 100644 index 0000000000..13a0b076e6 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/32_advanced-form-validator.json @@ -0,0 +1,166 @@ +{ + "name": "Advanced Form Validator", + "description": "Demonstrates complex validation logic and deeply nested formatting functions.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-advanced-validator", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-advanced-validator", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "welcome-text", + "email-field", + "phone-field", + "zip-field", + "terms-checkbox", + "submit-btn" + ], + "align": "stretch" + }, + { + "id": "welcome-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Hello! Today is ${formatDate(value: ${/now}, format: 'EEEE, MMMM d')}." + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "email-field", + "component": "TextField", + "label": "Email Address", + "value": {"path": "/formData/email"}, + "checks": [ + { + "condition": { + "call": "email", + "args": {"value": {"path": "/formData/email"}} + }, + "message": "Invalid email format" + } + ] + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone Number", + "value": {"path": "/formData/phone"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": { + "value": {"path": "/formData/phone"}, + "pattern": "^\\+?[0-9]{10,15}$" + } + }, + "message": "Invalid phone format" + } + ] + }, + { + "id": "zip-field", + "component": "TextField", + "label": "Zip Code", + "value": {"path": "/formData/zip"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": {"value": {"path": "/formData/zip"}, "pattern": "^[0-9]{5}$"} + }, + "message": "Must be exactly 5 digits" + } + ] + }, + { + "id": "terms-checkbox", + "component": "CheckBox", + "label": "I agree to the terms and conditions", + "value": {"path": "/formData/agree"} + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit Registration" + }, + { + "id": "submit-btn", + "component": "Button", + "child": "submit-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + {"path": "/formData/agree"}, + { + "call": "or", + "args": { + "values": [ + { + "call": "required", + "args": {"value": {"path": "/formData/email"}} + }, + { + "call": "required", + "args": {"value": {"path": "/formData/phone"}} + } + ] + } + }, + {"call": "required", "args": {"value": {"path": "/formData/zip"}}} + ] + } + }, + "message": "You must agree to terms AND provide either Email or Phone, plus a Zip code." + } + ], + "action": { + "event": { + "name": "register", + "context": {"data": {"path": "/formData"}} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-advanced-validator", + "value": { + "now": "2025-12-15T12:00:00Z", + "formData": { + "email": "", + "phone": "", + "zip": "", + "agree": false + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/33_financial-data-grid.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/33_financial-data-grid.json new file mode 100644 index 0000000000..5113d87883 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/33_financial-data-grid.json @@ -0,0 +1,171 @@ +{ + "name": "Financial Data Grid", + "description": "Demonstrates complex layout weighting and alignment using Rows and Columns with templates.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-financial-grid", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-financial-grid", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "grid-list"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["col-asset", "col-price", "col-change", "col-market-cap"], + "align": "center" + }, + { + "id": "col-asset", + "component": "Text", + "text": "Asset", + "variant": "caption", + "weight": 2 + }, + { + "id": "col-price", + "component": "Text", + "text": "Price", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-change", + "component": "Text", + "text": "24h Change", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-market-cap", + "component": "Text", + "text": "Market Cap", + "variant": "caption", + "weight": 1.5 + }, + { + "id": "grid-list", + "component": "List", + "children": { + "path": "/assets", + "componentId": "row-template" + } + }, + { + "id": "row-template", + "component": "Row", + "children": ["asset-info", "asset-price", "asset-change", "asset-market-cap"], + "align": "center" + }, + { + "id": "asset-info", + "component": "Row", + "children": ["asset-icon", "asset-name-col"], + "weight": 2, + "align": "center" + }, + { + "id": "asset-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "asset-name-col", + "component": "Column", + "children": ["asset-name", "asset-symbol"] + }, + { + "id": "asset-name", + "component": "Text", + "text": {"path": "name"}, + "variant": "body" + }, + { + "id": "asset-symbol", + "component": "Text", + "text": {"path": "symbol"}, + "variant": "caption" + }, + { + "id": "asset-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "price"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-change", + "component": "Text", + "text": { + "call": "formatString", + "args": {"value": "${change}%"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-market-cap", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "marketCap"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1.5 + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-financial-grid", + "value": { + "assets": [ + { + "name": "Bitcoin", + "symbol": "BTC", + "price": 43500.25, + "change": 1.2, + "marketCap": 850000000000 + }, + { + "name": "Ethereum", + "symbol": "ETH", + "price": 2250.5, + "change": -0.5, + "marketCap": 270000000000 + }, + { + "name": "Solana", + "symbol": "SOL", + "price": 95.8, + "change": 5.4, + "marketCap": 40000000000 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/34_child-list-template.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/34_child-list-template.json new file mode 100644 index 0000000000..adb9196e68 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/34_child-list-template.json @@ -0,0 +1,80 @@ +{ + "name": "ChildList Template Expansion", + "description": "Demonstrates dynamic list generation using ChildList object templates.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-child-list-template", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-child-list-template", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "item-list"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Dynamic Item List", + "variant": "h3" + }, + { + "id": "item-list", + "component": "List", + "children": { + "componentId": "item-row", + "path": "/items" + } + }, + { + "id": "item-row", + "component": "Row", + "children": ["item-name", "qty-label", "item-qty"] + }, + { + "id": "item-name", + "component": "Text", + "text": {"path": "name"} + }, + { + "id": "qty-label", + "component": "Text", + "text": " - Qty: " + }, + { + "id": "item-qty", + "component": "Text", + "text": {"path": "quantity"} + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-child-list-template", + "value": { + "items": [ + {"name": "Apple", "quantity": 10}, + {"name": "Banana", "quantity": 5}, + {"name": "Cherry", "quantity": 20} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/35_markdown-text.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/35_markdown-text.json new file mode 100644 index 0000000000..036b75f01b --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/35_markdown-text.json @@ -0,0 +1,44 @@ +{ + "name": "Markdown Text Support", + "description": "Demonstrates Markdown rendering in Text component.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-markdown-text", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-markdown-text", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "markdown-content"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Markdown Rendering", + "variant": "h3" + }, + { + "id": "markdown-content", + "component": "Text", + "text": "# Heading 1\n\nThis is **bold** text and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link to Google](https://google.com)" + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/36_modal.json b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/36_modal.json new file mode 100644 index 0000000000..84902f57df --- /dev/null +++ b/agent_sdks/go/a2ui/v010/testdata/v0_10/catalogs/basic/examples/36_modal.json @@ -0,0 +1,65 @@ +{ + "name": "Modal Sample", + "description": "Example of Modal component showing a trigger and content.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "modal-sample-surface", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "modal-sample-surface", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["title", "modal-comp"] + }, + { + "id": "title", + "component": "Text", + "text": "Modal Component Sample", + "variant": "h2" + }, + { + "id": "modal-comp", + "component": "Modal", + "trigger": "open-btn", + "content": "modal-content" + }, + { + "id": "open-btn-text", + "component": "Text", + "text": "Open Modal" + }, + { + "id": "open-btn", + "component": "Button", + "child": "open-btn-text", + "action": { + "event": { + "name": "openModalEvent", + "context": {} + } + } + }, + { + "id": "modal-content", + "component": "Column", + "children": ["modal-text"] + }, + { + "id": "modal-text", + "component": "Text", + "text": "This is the content inside the modal." + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v010/zz_component.go b/agent_sdks/go/a2ui/v010/zz_component.go new file mode 100644 index 0000000000..138116d1aa --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_component.go @@ -0,0 +1,138 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +// TabDef defines a tab within a [Tabs] component. +type TabDef struct { + Title DynamicString `json:"title"` + Child string `json:"child"` +} + +// ChoiceOption defines a selectable option within a [ChoicePicker] component. +type ChoiceOption struct { + Label DynamicString `json:"label"` + Value string `json:"value"` +} + +// AudioPlayerComponent holds the component-specific fields for a AudioPlayer. +type AudioPlayerComponent struct { + Description *DynamicString `json:"description,omitempty"` + URL DynamicString `json:"url"` +} + +// ButtonComponent holds the component-specific fields for a Button. +type ButtonComponent struct { + Action Action `json:"action"` + Child string `json:"child"` + Variant ButtonVariant `json:"variant,omitempty"` +} + +// CardComponent holds the component-specific fields for a Card. +type CardComponent struct { + Child string `json:"child"` +} + +// CheckBoxComponent holds the component-specific fields for a CheckBox. +type CheckBoxComponent struct { + Label DynamicString `json:"label"` + Value DynamicBoolean `json:"value"` +} + +// ChoicePickerComponent holds the component-specific fields for a ChoicePicker. +type ChoicePickerComponent struct { + DisplayStyle ChoicePickerDisplayStyle `json:"displayStyle,omitempty"` + Filterable *bool `json:"filterable,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Options []ChoiceOption `json:"options"` + Value DynamicStringList `json:"value"` + Variant ChoicePickerVariant `json:"variant,omitempty"` +} + +// ColumnComponent holds the component-specific fields for a Column. +type ColumnComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// DateTimeInputComponent holds the component-specific fields for a DateTimeInput. +type DateTimeInputComponent struct { + EnableDate *bool `json:"enableDate,omitempty"` + EnableTime *bool `json:"enableTime,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Max *DynamicString `json:"max,omitempty"` + Min *DynamicString `json:"min,omitempty"` + Value DynamicString `json:"value"` +} + +// DividerComponent holds the component-specific fields for a Divider. +type DividerComponent struct { + Axis DividerAxis `json:"axis,omitempty"` +} + +// IconComponent holds the component-specific fields for a Icon. +type IconComponent struct { + Name IconNameOrPath `json:"name"` +} + +// ImageComponent holds the component-specific fields for a Image. +type ImageComponent struct { + Description *DynamicString `json:"description,omitempty"` + Fit ImageFit `json:"fit,omitempty"` + URL DynamicString `json:"url"` + Variant ImageVariant `json:"variant,omitempty"` +} + +// ListComponent holds the component-specific fields for a List. +type ListComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Direction ListDirection `json:"direction,omitempty"` +} + +// ModalComponent holds the component-specific fields for a Modal. +type ModalComponent struct { + Content string `json:"content"` + Trigger string `json:"trigger"` +} + +// RowComponent holds the component-specific fields for a Row. +type RowComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// SliderComponent holds the component-specific fields for a Slider. +type SliderComponent struct { + Label *DynamicString `json:"label,omitempty"` + Max float64 `json:"max"` + Min *float64 `json:"min,omitempty"` + Steps *int `json:"steps,omitempty"` + Value DynamicNumber `json:"value"` +} + +// TabsComponent holds the component-specific fields for a Tabs. +type TabsComponent struct { + Tabs []TabDef `json:"tabs"` +} + +// TextComponent holds the component-specific fields for a Text. +type TextComponent struct { + Text DynamicString `json:"text"` + Variant TextVariant `json:"variant,omitempty"` +} + +// TextFieldComponent holds the component-specific fields for a TextField. +type TextFieldComponent struct { + Label DynamicString `json:"label"` + Placeholder *DynamicString `json:"placeholder,omitempty"` + Value *DynamicString `json:"value,omitempty"` + Variant TextFieldVariant `json:"variant,omitempty"` +} + +// VideoComponent holds the component-specific fields for a Video. +type VideoComponent struct { + PosterURL *DynamicString `json:"posterUrl,omitempty"` + URL DynamicString `json:"url"` +} diff --git a/agent_sdks/go/a2ui/v010/zz_component_marshal.go b/agent_sdks/go/a2ui/v010/zz_component_marshal.go new file mode 100644 index 0000000000..bef0172457 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_component_marshal.go @@ -0,0 +1,200 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON encodes a [Component] as a flat JSON object with the "component" +// discriminator, common fields, and component-specific fields merged together. +func (c Component) MarshalJSON() ([]byte, error) { + type common struct { + ComponentType string `json:"component"` + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + componentType, specific, count := c.componentData() + switch count { + case 0: + return nil, fmt.Errorf("a2ui: component has no concrete type set") + case 1: + default: + return nil, fmt.Errorf("a2ui: component has multiple concrete types set") + } + cm := common{ + ComponentType: componentType, + ID: c.ID, + Accessibility: c.Accessibility, + Weight: c.Weight, + Checks: c.Checks, + } + + commonBytes, err := json.Marshal(cm) + if err != nil { + return nil, err + } + + specificBytes, err := json.Marshal(specific) + if err != nil { + return nil, err + } + + // Merge: overwrite common with specific fields. + if len(specificBytes) <= 2 { // "{}" or empty + return commonBytes, nil + } + // Remove leading '{' from specific, append to common. + merged := make([]byte, 0, len(commonBytes)+len(specificBytes)) + merged = append(merged, commonBytes[:len(commonBytes)-1]...) // drop trailing '}' + merged = append(merged, ',') + merged = append(merged, specificBytes[1:]...) // drop leading '{' + return merged, nil +} + +// UnmarshalJSON decodes a flat JSON object into a [Component], using the +// "component" field as the discriminator. +func (c *Component) UnmarshalJSON(data []byte) error { + // Read the discriminator. + var disc struct { + ComponentType string `json:"component"` + } + if err := json.Unmarshal(data, &disc); err != nil { + return err + } + + // Unmarshal common fields. + type commonOnly struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + var cm commonOnly + if err := json.Unmarshal(data, &cm); err != nil { + return err + } + *c = Component{} + c.ID = cm.ID + c.Accessibility = cm.Accessibility + c.Weight = cm.Weight + c.Checks = cm.Checks + + // Unmarshal component-specific fields. + switch disc.ComponentType { + case "AudioPlayer": + var v AudioPlayerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.AudioPlayer = &v + case "Button": + var v ButtonComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Button = &v + case "Card": + var v CardComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Card = &v + case "CheckBox": + var v CheckBoxComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.CheckBox = &v + case "ChoicePicker": + var v ChoicePickerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.ChoicePicker = &v + case "Column": + var v ColumnComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Column = &v + case "DateTimeInput": + var v DateTimeInputComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.DateTimeInput = &v + case "Divider": + var v DividerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Divider = &v + case "Icon": + var v IconComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Icon = &v + case "Image": + var v ImageComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Image = &v + case "List": + var v ListComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.List = &v + case "Modal": + var v ModalComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Modal = &v + case "Row": + var v RowComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Row = &v + case "Slider": + var v SliderComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Slider = &v + case "Tabs": + var v TabsComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Tabs = &v + case "Text": + var v TextComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Text = &v + case "TextField": + var v TextFieldComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.TextField = &v + case "Video": + var v VideoComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Video = &v + default: + return fmt.Errorf("unknown component type: %s", disc.ComponentType) + } + return nil +} diff --git a/agent_sdks/go/a2ui/v010/zz_enum.go b/agent_sdks/go/a2ui/v010/zz_enum.go new file mode 100644 index 0000000000..3de4db54dc --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_enum.go @@ -0,0 +1,128 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +// ReturnType is the expected return type of a function call. +type ReturnType string + +const ( + ReturnTypeArray ReturnType = "array" + ReturnTypeBoolean ReturnType = "boolean" + ReturnTypeNumber ReturnType = "number" + ReturnTypeObject ReturnType = "object" + ReturnTypeString ReturnType = "string" + ReturnTypeVoid ReturnType = "void" +) + +// IconName identifies a built-in icon. +type IconName string + +// ButtonVariant defines the allowed values for the ButtonVariant enum. +type ButtonVariant string + +const ( + ButtonVariantDefault ButtonVariant = "default" + ButtonVariantPrimary ButtonVariant = "primary" + ButtonVariantBorderless ButtonVariant = "borderless" +) + +// ChoicePickerDisplayStyle defines the allowed values for the ChoicePickerDisplayStyle enum. +type ChoicePickerDisplayStyle string + +const ( + ChoicePickerDisplayStyleCheckbox ChoicePickerDisplayStyle = "checkbox" + ChoicePickerDisplayStyleChips ChoicePickerDisplayStyle = "chips" +) + +// ChoicePickerVariant defines the allowed values for the ChoicePickerVariant enum. +type ChoicePickerVariant string + +const ( + ChoicePickerVariantMultipleSelection ChoicePickerVariant = "multipleSelection" + ChoicePickerVariantMutuallyExclusive ChoicePickerVariant = "mutuallyExclusive" +) + +// DividerAxis defines the allowed values for the DividerAxis enum. +type DividerAxis string + +const ( + DividerAxisHorizontal DividerAxis = "horizontal" + DividerAxisVertical DividerAxis = "vertical" +) + +// ImageFit defines the allowed values for the ImageFit enum. +type ImageFit string + +const ( + ImageFitContain ImageFit = "contain" + ImageFitCover ImageFit = "cover" + ImageFitFill ImageFit = "fill" + ImageFitNone ImageFit = "none" + ImageFitScaleDown ImageFit = "scaleDown" +) + +// ImageVariant defines the allowed values for the ImageVariant enum. +type ImageVariant string + +const ( + ImageVariantIcon ImageVariant = "icon" + ImageVariantAvatar ImageVariant = "avatar" + ImageVariantSmallFeature ImageVariant = "smallFeature" + ImageVariantMediumFeature ImageVariant = "mediumFeature" + ImageVariantLargeFeature ImageVariant = "largeFeature" + ImageVariantHeader ImageVariant = "header" +) + +// LayoutAlign defines the allowed values for the LayoutAlign enum. +type LayoutAlign string + +const ( + LayoutAlignCenter LayoutAlign = "center" + LayoutAlignEnd LayoutAlign = "end" + LayoutAlignStart LayoutAlign = "start" + LayoutAlignStretch LayoutAlign = "stretch" +) + +// LayoutJustify defines the allowed values for the LayoutJustify enum. +type LayoutJustify string + +const ( + LayoutJustifyStart LayoutJustify = "start" + LayoutJustifyCenter LayoutJustify = "center" + LayoutJustifyEnd LayoutJustify = "end" + LayoutJustifySpaceBetween LayoutJustify = "spaceBetween" + LayoutJustifySpaceAround LayoutJustify = "spaceAround" + LayoutJustifySpaceEvenly LayoutJustify = "spaceEvenly" + LayoutJustifyStretch LayoutJustify = "stretch" +) + +// ListDirection defines the allowed values for the ListDirection enum. +type ListDirection string + +const ( + ListDirectionVertical ListDirection = "vertical" + ListDirectionHorizontal ListDirection = "horizontal" +) + +// TextFieldVariant defines the allowed values for the TextFieldVariant enum. +type TextFieldVariant string + +const ( + TextFieldVariantLongText TextFieldVariant = "longText" + TextFieldVariantNumber TextFieldVariant = "number" + TextFieldVariantShortText TextFieldVariant = "shortText" + TextFieldVariantObscured TextFieldVariant = "obscured" +) + +// TextVariant defines the allowed values for the TextVariant enum. +type TextVariant string + +const ( + TextVariantH1 TextVariant = "h1" + TextVariantH2 TextVariant = "h2" + TextVariantH3 TextVariant = "h3" + TextVariantH4 TextVariant = "h4" + TextVariantH5 TextVariant = "h5" + TextVariantCaption TextVariant = "caption" + TextVariantBody TextVariant = "body" +) diff --git a/agent_sdks/go/a2ui/v010/zz_function.go b/agent_sdks/go/a2ui/v010/zz_function.go new file mode 100644 index 0000000000..122086a4dc --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_function.go @@ -0,0 +1,188 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +// And creates a function call for "and". +// Performs a logical AND operation on a list of boolean values. +func And(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "and", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Email creates a function call for "email". +// Checks that the value is a valid email address. +func Email(value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "email", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// FormatCurrency creates a function call for "formatCurrency". +// Formats a number as a currency string. +func FormatCurrency(currency DynamicString, decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatCurrency", + Args: map[string]any{ + "currency": currency, + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatDate creates a function call for "formatDate". +// Formats a timestamp into a string using a pattern. +func FormatDate(format DynamicString, value DynamicValue) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatDate", + Args: map[string]any{ + "format": format, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatNumber creates a function call for "formatNumber". +// Formats a number with the specified grouping and decimal precision. +func FormatNumber(decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatNumber", + Args: map[string]any{ + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatString creates a function call for "formatString". +// Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\${`. +func FormatString(value DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatString", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// Length creates a function call for "length". +// Checks string length constraints. +func Length(max int, min int, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "length", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Not creates a function call for "not". +// Performs a logical NOT operation on a boolean value. +func Not(value DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "not", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Numeric creates a function call for "numeric". +// Checks numeric range constraints. +func Numeric(max float64, min float64, value DynamicNumber) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "numeric", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// OpenURL creates a function call for "openUrl". +// Opens the specified URL in a browser or handler. This function has no return value. +func OpenURL(url string) Action { + return Action{FunctionCall: &FunctionCall{ + Call: "openUrl", + Args: map[string]any{ + "url": url, + }, + ReturnType: ReturnTypeVoid, + }} +} + +// Or creates a function call for "or". +// Performs a logical OR operation on a list of boolean values. +func Or(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "or", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Pluralize creates a function call for "pluralize". +// Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'. +func Pluralize(few DynamicString, many DynamicString, one DynamicString, other DynamicString, two DynamicString, value DynamicNumber, zero DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "pluralize", + Args: map[string]any{ + "few": few, + "many": many, + "one": one, + "other": other, + "two": two, + "value": value, + "zero": zero, + }, + ReturnType: ReturnTypeString, + }} +} + +// Regex creates a function call for "regex". +// Checks that the value matches a regular expression string. +func Regex(pattern string, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "regex", + Args: map[string]any{ + "pattern": pattern, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Required creates a function call for "required". +// Checks that the value is not null, undefined, or empty. +func Required(value DynamicValue) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "required", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} diff --git a/agent_sdks/go/a2ui/v010/zz_icon.go b/agent_sdks/go/a2ui/v010/zz_icon.go new file mode 100644 index 0000000000..358a6682a7 --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_icon.go @@ -0,0 +1,66 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +// Well-known icon names. +const ( + IconAccountCircle IconName = "accountCircle" + IconAdd IconName = "add" + IconArrowBack IconName = "arrowBack" + IconArrowForward IconName = "arrowForward" + IconAttachFile IconName = "attachFile" + IconCalendarToday IconName = "calendarToday" + IconCall IconName = "call" + IconCamera IconName = "camera" + IconCheck IconName = "check" + IconClose IconName = "close" + IconDelete IconName = "delete" + IconDownload IconName = "download" + IconEdit IconName = "edit" + IconEvent IconName = "event" + IconError IconName = "error" + IconFastForward IconName = "fastForward" + IconFavorite IconName = "favorite" + IconFavoriteOff IconName = "favoriteOff" + IconFolder IconName = "folder" + IconHelp IconName = "help" + IconHome IconName = "home" + IconInfo IconName = "info" + IconLocationOn IconName = "locationOn" + IconLock IconName = "lock" + IconLockOpen IconName = "lockOpen" + IconMail IconName = "mail" + IconMenu IconName = "menu" + IconMoreVert IconName = "moreVert" + IconMoreHoriz IconName = "moreHoriz" + IconNotificationsOff IconName = "notificationsOff" + IconNotifications IconName = "notifications" + IconPause IconName = "pause" + IconPayment IconName = "payment" + IconPerson IconName = "person" + IconPhone IconName = "phone" + IconPhoto IconName = "photo" + IconPlay IconName = "play" + IconPrint IconName = "print" + IconRefresh IconName = "refresh" + IconRewind IconName = "rewind" + IconSearch IconName = "search" + IconSend IconName = "send" + IconSettings IconName = "settings" + IconShare IconName = "share" + IconShoppingCart IconName = "shoppingCart" + IconSkipNext IconName = "skipNext" + IconSkipPrevious IconName = "skipPrevious" + IconStar IconName = "star" + IconStarHalf IconName = "starHalf" + IconStarOff IconName = "starOff" + IconStop IconName = "stop" + IconUpload IconName = "upload" + IconVisibility IconName = "visibility" + IconVisibilityOff IconName = "visibilityOff" + IconVolumeDown IconName = "volumeDown" + IconVolumeMute IconName = "volumeMute" + IconVolumeOff IconName = "volumeOff" + IconVolumeUp IconName = "volumeUp" + IconWarning IconName = "warning" +) diff --git a/agent_sdks/go/a2ui/v010/zz_wrapper.go b/agent_sdks/go/a2ui/v010/zz_wrapper.go new file mode 100644 index 0000000000..6c1f31af9f --- /dev/null +++ b/agent_sdks/go/a2ui/v010/zz_wrapper.go @@ -0,0 +1,15 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v010 + +// ClientMessageListWrapper wraps a list of [ClientMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ClientMessageListWrapper struct { + Messages []ClientMessage `json:"messages"` +} + +// ServerMessageListWrapper wraps a list of [ServerMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ServerMessageListWrapper struct { + Messages []ServerMessage `json:"messages"` +} diff --git a/agent_sdks/go/a2ui/v09/capabilities.go b/agent_sdks/go/a2ui/v09/capabilities.go new file mode 100644 index 0000000000..d1c6e787ca --- /dev/null +++ b/agent_sdks/go/a2ui/v09/capabilities.go @@ -0,0 +1,50 @@ +package v09 + +import "encoding/json" + +// ClientCapabilities describes a client's UI rendering capabilities, +// sent as part of A2A metadata. +type ClientCapabilities struct { + V09 *ClientCapabilitiesV09 `json:"v0.9,omitempty"` +} + +// ClientCapabilitiesV09 is the v0.9 client capabilities structure. +type ClientCapabilitiesV09 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds"` + InlineCatalogs []CatalogDef `json:"inlineCatalogs,omitempty"` +} + +// ServerCapabilities describes an agent's supported UI features, +// advertised via agent card or other discovery. +type ServerCapabilities struct { + V09 *ServerCapabilitiesV09 `json:"v0.9,omitempty"` +} + +// ServerCapabilitiesV09 is the v0.9 server capabilities structure. +type ServerCapabilitiesV09 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds,omitempty"` + AcceptsInlineCatalogs bool `json:"acceptsInlineCatalogs,omitempty"` +} + +// CatalogDef is an inline catalog definition containing component schemas +// and function definitions. +type CatalogDef struct { + CatalogID string `json:"catalogId"` + Components map[string]json.RawMessage `json:"components,omitempty"` + Functions []FunctionDefinition `json:"functions,omitempty"` + Theme map[string]json.RawMessage `json:"theme,omitempty"` +} + +// FunctionDefinition describes a function's interface for catalog definitions. +type FunctionDefinition struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Parameters json.RawMessage `json:"parameters"` + ReturnType ReturnType `json:"returnType"` +} + +// ClientDataModel carries the client data model in A2A message metadata. +type ClientDataModel struct { + Version string `json:"version"` + Surfaces map[string]map[string]any `json:"surfaces"` +} diff --git a/agent_sdks/go/a2ui/v09/common.go b/agent_sdks/go/a2ui/v09/common.go new file mode 100644 index 0000000000..69b29cba47 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/common.go @@ -0,0 +1,67 @@ +package v09 + +// DataBinding references a value in the client data model by JSON Pointer path. +type DataBinding struct { + Path string `json:"path"` +} + +// FunctionCall invokes a named client-side function. +type FunctionCall struct { + Call string `json:"call"` + Args map[string]any `json:"args,omitempty"` + ReturnType ReturnType `json:"returnType,omitempty"` +} + +// ChildList is either a static list of component IDs or a dynamic template. +// Exactly one of IDs or Template is set. +type ChildList struct { + IDs []string + Template *ChildTemplate +} + +// ChildTemplate generates a dynamic list of children from a data model list. +type ChildTemplate struct { + ComponentID string `json:"componentId"` + Path string `json:"path"` +} + +// CheckRule is a single validation rule applied to an input component. +type CheckRule struct { + Condition DynamicBoolean `json:"condition"` + Message string `json:"message"` +} + +// AccessibilityAttributes enhance accessibility for assistive technologies. +type AccessibilityAttributes struct { + Label *DynamicString `json:"label,omitempty"` + Description *DynamicString `json:"description,omitempty"` +} + +// Theme defines visual theming for a surface. +type Theme struct { + PrimaryColor string `json:"primaryColor,omitempty"` + IconURL string `json:"iconUrl,omitempty"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AdditionalProperties map[string]any `json:"-"` +} + +// Action is an interaction handler that either triggers a server-side event +// or executes a client-side function. Exactly one field is non-nil. +type Action struct { + Event *EventAction `json:"event,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` +} + +// EventAction triggers a server-side event. +type EventAction struct { + Name string `json:"name"` + Context map[string]DynamicValue `json:"context,omitempty"` +} + +// IconNameOrPath is a well-known icon name, custom SVG path, or binding. +// Exactly one field is non-nil. +type IconNameOrPath struct { + Name *IconName + SVGPath *string + Binding *DataBinding +} diff --git a/agent_sdks/go/a2ui/v09/common_json.go b/agent_sdks/go/a2ui/v09/common_json.go new file mode 100644 index 0000000000..851ec84c34 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/common_json.go @@ -0,0 +1,183 @@ +package v09 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for Theme. +func (t Theme) MarshalJSON() ([]byte, error) { + fields := make(map[string]any, len(t.AdditionalProperties)+3) + for k, v := range t.AdditionalProperties { + fields[k] = v + } + if t.PrimaryColor != "" { + fields["primaryColor"] = t.PrimaryColor + } + if t.IconURL != "" { + fields["iconUrl"] = t.IconURL + } + if t.AgentDisplayName != "" { + fields["agentDisplayName"] = t.AgentDisplayName + } + return json.Marshal(fields) +} + +// UnmarshalJSON implements json.Unmarshaler for Theme. +func (t *Theme) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return fmt.Errorf("a2ui: unmarshal theme: %w", err) + } + *t = Theme{} + for key, raw := range fields { + switch key { + case "primaryColor": + if err := json.Unmarshal(raw, &t.PrimaryColor); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.primaryColor: %w", err) + } + case "iconUrl": + if err := json.Unmarshal(raw, &t.IconURL); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.iconUrl: %w", err) + } + case "agentDisplayName": + if err := json.Unmarshal(raw, &t.AgentDisplayName); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.agentDisplayName: %w", err) + } + default: + if t.AdditionalProperties == nil { + t.AdditionalProperties = make(map[string]any) + } + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.%s: %w", key, err) + } + t.AdditionalProperties[key] = value + } + } + return nil +} + +// MarshalJSON implements json.Marshaler for ChildList. +func (c ChildList) MarshalJSON() ([]byte, error) { + if c.Template != nil && c.IDs != nil { + return nil, fmt.Errorf("a2ui: ChildList has both ids and template set") + } + switch { + case c.Template != nil: + return json.Marshal(c.Template) + case c.IDs != nil: + return json.Marshal(c.IDs) + default: + return []byte("[]"), nil + } +} + +// UnmarshalJSON implements json.Unmarshaler for ChildList. +func (c *ChildList) UnmarshalJSON(data []byte) error { + *c = ChildList{} + var ids []string + if err := json.Unmarshal(data, &ids); err == nil { + c.IDs = ids + return nil + } + var t ChildTemplate + if err := json.Unmarshal(data, &t); err != nil { + return fmt.Errorf("a2ui: unmarshal child list: %w", err) + } + c.Template = &t + return nil +} + +// MarshalJSON implements json.Marshaler for Action. +func (a Action) MarshalJSON() ([]byte, error) { + type actionAlias Action + switch countSet(a.Event != nil, a.FunctionCall != nil) { + case 1: + return json.Marshal(actionAlias(a)) + case 0: + return nil, fmt.Errorf("a2ui: Action has no value set") + default: + return nil, fmt.Errorf("a2ui: Action has multiple values set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for Action. +func (a *Action) UnmarshalJSON(data []byte) error { + type actionAlias Action + var aa actionAlias + if err := json.Unmarshal(data, &aa); err != nil { + return fmt.Errorf("a2ui: unmarshal action: %w", err) + } + switch countSet(aa.Event != nil, aa.FunctionCall != nil) { + case 1: + *a = Action(aa) + return nil + case 0: + return fmt.Errorf("a2ui: action must have event or functionCall") + default: + return fmt.Errorf("a2ui: action must not have both event and functionCall") + } +} + +// MarshalJSON implements json.Marshaler for IconNameOrPath. +func (i IconNameOrPath) MarshalJSON() ([]byte, error) { + switch countSet(i.Name != nil, i.SVGPath != nil, i.Binding != nil) { + case 1: + switch { + case i.Name != nil: + return json.Marshal(string(*i.Name)) + case i.SVGPath != nil: + return json.Marshal(struct { + SVGPath string `json:"svgPath"` + }{SVGPath: *i.SVGPath}) + case i.Binding != nil: + return json.Marshal(i.Binding) + } + case 0: + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") + default: + return nil, fmt.Errorf("a2ui: IconNameOrPath has multiple values set") + } + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") +} + +// UnmarshalJSON implements json.Unmarshaler for IconNameOrPath. +func (i *IconNameOrPath) UnmarshalJSON(data []byte) error { + *i = IconNameOrPath{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + name := IconName(s) + i.Name = &name + return nil + } + var obj struct { + SVGPath string `json:"svgPath"` + Path string `json:"path"` + } + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: unmarshal icon name or path: %w", err) + } + switch { + case obj.SVGPath != "" && obj.Path != "": + return fmt.Errorf("a2ui: icon name must not have both svgPath and path") + case obj.SVGPath != "": + i.SVGPath = &obj.SVGPath + return nil + case obj.Path != "": + i.Binding = &DataBinding{Path: obj.Path} + return nil + default: + return fmt.Errorf("a2ui: icon path must not be empty") + } +} + +func countSet(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} diff --git a/agent_sdks/go/a2ui/v09/common_test.go b/agent_sdks/go/a2ui/v09/common_test.go new file mode 100644 index 0000000000..c74d2f5bc4 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/common_test.go @@ -0,0 +1,51 @@ +package v09 + +import ( + "encoding/json" + "testing" +) + +func TestActionRejectsInvalidStates(t *testing.T) { + action := Action{ + Event: &EventAction{Name: "submit"}, + FunctionCall: &FunctionCall{Call: "openUrl"}, + } + if _, err := json.Marshal(action); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded Action + if err := json.Unmarshal([]byte(`{"event":{"name":"submit"},"functionCall":{"call":"openUrl"}}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestIconNameOrPathRejectsInvalidStates(t *testing.T) { + path := "/tmp/icon.svg" + name := IconSearch + icon := IconNameOrPath{Name: &name, SVGPath: &path} + if _, err := json.Marshal(icon); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded IconNameOrPath + if err := json.Unmarshal([]byte(`{"svgPath":""}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestIconNameOrPathV09Forms(t *testing.T) { + path := "M0 0h1v1z" + roundTrip(t, IconNameOrPath{SVGPath: &path}, `{"svgPath":"M0 0h1v1z"}`) + roundTrip(t, IconNameOrPath{Binding: &DataBinding{Path: "/icon"}}, `{"path":"/icon"}`) +} + +func TestChildListRejectsMultipleRepresentations(t *testing.T) { + children := ChildList{ + IDs: []string{"a"}, + Template: &ChildTemplate{ComponentID: "child", Path: "/items"}, + } + if _, err := json.Marshal(children); err == nil { + t.Fatal("expected marshal error, got nil") + } +} diff --git a/agent_sdks/go/a2ui/v09/component.go b/agent_sdks/go/a2ui/v09/component.go new file mode 100644 index 0000000000..1a906f82b9 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/component.go @@ -0,0 +1,110 @@ +package v09 + +// Component represents any A2UI component in the component tree. +// Exactly one of the concrete type fields is non-nil. +// +// MarshalJSON/UnmarshalJSON in zz_component_marshal.go handle +// serialization, using the "component" field as a discriminator. +type Component struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + + // Concrete type fields (exactly one non-nil). + Text *TextComponent `json:"-"` + Image *ImageComponent `json:"-"` + Icon *IconComponent `json:"-"` + Video *VideoComponent `json:"-"` + AudioPlayer *AudioPlayerComponent `json:"-"` + Row *RowComponent `json:"-"` + Column *ColumnComponent `json:"-"` + List *ListComponent `json:"-"` + Card *CardComponent `json:"-"` + Tabs *TabsComponent `json:"-"` + Modal *ModalComponent `json:"-"` + Divider *DividerComponent `json:"-"` + Button *ButtonComponent `json:"-"` + TextField *TextFieldComponent `json:"-"` + CheckBox *CheckBoxComponent `json:"-"` + ChoicePicker *ChoicePickerComponent `json:"-"` + Slider *SliderComponent `json:"-"` + DateTimeInput *DateTimeInputComponent `json:"-"` +} + +func (c Component) componentData() (string, any, int) { + var ( + componentType string + specific any + count int + ) + set := func(typ string, value any) { + componentType = typ + specific = value + count++ + } + if c.Text != nil { + set("Text", c.Text) + } + if c.Image != nil { + set("Image", c.Image) + } + if c.Icon != nil { + set("Icon", c.Icon) + } + if c.Video != nil { + set("Video", c.Video) + } + if c.AudioPlayer != nil { + set("AudioPlayer", c.AudioPlayer) + } + if c.Row != nil { + set("Row", c.Row) + } + if c.Column != nil { + set("Column", c.Column) + } + if c.List != nil { + set("List", c.List) + } + if c.Card != nil { + set("Card", c.Card) + } + if c.Tabs != nil { + set("Tabs", c.Tabs) + } + if c.Modal != nil { + set("Modal", c.Modal) + } + if c.Divider != nil { + set("Divider", c.Divider) + } + if c.Button != nil { + set("Button", c.Button) + } + if c.TextField != nil { + set("TextField", c.TextField) + } + if c.CheckBox != nil { + set("CheckBox", c.CheckBox) + } + if c.ChoicePicker != nil { + set("ChoicePicker", c.ChoicePicker) + } + if c.Slider != nil { + set("Slider", c.Slider) + } + if c.DateTimeInput != nil { + set("DateTimeInput", c.DateTimeInput) + } + return componentType, specific, count +} + +// ComponentType returns the discriminator string (e.g. "Text", "Button"). +func (c Component) ComponentType() string { + componentType, _, count := c.componentData() + if count != 1 { + return "" + } + return componentType +} diff --git a/agent_sdks/go/a2ui/v09/component_test.go b/agent_sdks/go/a2ui/v09/component_test.go new file mode 100644 index 0000000000..01ccc83deb --- /dev/null +++ b/agent_sdks/go/a2ui/v09/component_test.go @@ -0,0 +1,254 @@ +package v09 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +const basicExamplesDir = "testdata/v0_9/catalogs/basic/examples" + +func TestLoginFormComponents(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/09_login-form.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + + // Second message is updateComponents. + var msg ServerMessage + if err := json.Unmarshal(example.Messages[1], &msg); err != nil { + t.Fatal(err) + } + if msg.UpdateComponents == nil { + t.Fatal("expected updateComponents") + } + + components := msg.UpdateComponents.Components + byID := make(map[string]*Component, len(components)) + for i := range components { + byID[components[i].ID] = &components[i] + } + + t.Run("TextField", func(t *testing.T) { + c, ok := byID["email-field"] + if !ok { + t.Fatal("missing email-field") + } + if c.ComponentType() != "TextField" { + t.Fatalf("type = %q, want TextField", c.ComponentType()) + } + if c.TextField == nil { + t.Fatal("TextField is nil") + } + if c.TextField.Label.Literal == nil || *c.TextField.Label.Literal != "Email" { + t.Fatalf("label = %+v, want literal Email", c.TextField.Label) + } + if c.TextField.Value == nil || c.TextField.Value.Binding == nil || c.TextField.Value.Binding.Path != "/email" { + t.Fatal("value should bind to /email") + } + }) + + t.Run("Button", func(t *testing.T) { + c, ok := byID["login-btn"] + if !ok { + t.Fatal("missing login-btn") + } + if c.ComponentType() != "Button" { + t.Fatalf("type = %q, want Button", c.ComponentType()) + } + if c.Button.Child != "login-btn-text" { + t.Fatalf("child = %q", c.Button.Child) + } + if c.Button.Action.Event == nil { + t.Fatal("expected event action") + } + if c.Button.Action.Event.Name != "login" { + t.Fatalf("event name = %q", c.Button.Action.Event.Name) + } + }) + + t.Run("Column", func(t *testing.T) { + c, ok := byID["main-column"] + if !ok { + t.Fatal("missing main-column") + } + if c.ComponentType() != "Column" { + t.Fatalf("type = %q, want Column", c.ComponentType()) + } + if len(c.Column.Children.IDs) != 6 { + t.Fatalf("children = %d, want 6", len(c.Column.Children.IDs)) + } + }) + + t.Run("CheckRule", func(t *testing.T) { + c := byID["email-field"] + if len(c.Checks) != 2 { + t.Fatalf("checks = %d, want 2", len(c.Checks)) + } + if c.Checks[0].Message != "Email is required" { + t.Fatalf("message = %q", c.Checks[0].Message) + } + if c.Checks[0].Condition.FunctionCall == nil { + t.Fatal("expected function call condition") + } + if c.Checks[0].Condition.FunctionCall.Call != "required" { + t.Fatalf("call = %q", c.Checks[0].Condition.FunctionCall.Call) + } + }) + + t.Run("Card", func(t *testing.T) { + c, ok := byID["root"] + if !ok { + t.Fatal("missing root") + } + if c.ComponentType() != "Card" { + t.Fatalf("type = %q, want Card", c.ComponentType()) + } + if c.Card.Child != "main-column" { + t.Fatalf("child = %q", c.Card.Child) + } + }) +} + +func TestComponentTypeDiscriminator(t *testing.T) { + tests := []struct { + name string + comp Component + want string + }{ + {"Text", Component{Text: &TextComponent{}}, "Text"}, + {"Button", Component{Button: &ButtonComponent{}}, "Button"}, + {"Column", Component{Column: &ColumnComponent{}}, "Column"}, + {"Row", Component{Row: &RowComponent{}}, "Row"}, + {"Card", Component{Card: &CardComponent{}}, "Card"}, + {"Image", Component{Image: &ImageComponent{}}, "Image"}, + {"Icon", Component{Icon: &IconComponent{}}, "Icon"}, + {"TextField", Component{TextField: &TextFieldComponent{}}, "TextField"}, + {"CheckBox", Component{CheckBox: &CheckBoxComponent{}}, "CheckBox"}, + {"Divider", Component{Divider: &DividerComponent{}}, "Divider"}, + {"Slider", Component{Slider: &SliderComponent{}}, "Slider"}, + {"Tabs", Component{Tabs: &TabsComponent{}}, "Tabs"}, + {"Modal", Component{Modal: &ModalComponent{}}, "Modal"}, + {"List", Component{List: &ListComponent{}}, "List"}, + {"multiple", Component{Text: &TextComponent{}, Button: &ButtonComponent{}}, ""}, + {"empty", Component{}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.comp.ComponentType(); got != tt.want { + t.Fatalf("ComponentType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestComponentRoundTrip(t *testing.T) { + tests := []struct { + name string + json string + }{ + { + name: "text", + json: `{"component":"Text","id":"t1","text":"hello","variant":"h1"}`, + }, + { + name: "button_with_event", + json: `{"component":"Button","id":"b1","child":"b1-text","action":{"event":{"name":"click"}}}`, + }, + { + name: "column", + json: `{"component":"Column","id":"c1","children":["a","b","c"],"align":"center"}`, + }, + { + name: "text_with_binding", + json: `{"component":"Text","id":"t2","text":{"path":"/name"}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var c Component + if err := json.Unmarshal([]byte(tt.json), &c); err != nil { + t.Fatalf("unmarshal: %v", err) + } + roundTrip(t, c, tt.json) + }) + } +} + +func TestComponentMarshalRejectsInvalidConcreteTypes(t *testing.T) { + tests := []struct { + name string + comp Component + }{ + { + name: "none", + comp: Component{ID: "empty"}, + }, + { + name: "multiple", + comp: Component{ + ID: "bad", + Text: &TextComponent{Text: StringLiteral("hello")}, + Button: &ButtonComponent{Action: Action{Event: &EventAction{Name: "click"}}, Child: "child"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.comp); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestAllExamplesUnmarshal(t *testing.T) { + entries, err := os.ReadDir(basicExamplesDir) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + t.Run(e.Name(), func(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/" + e.Name()) + if err != nil { + t.Fatal(err) + } + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + for i, raw := range example.Messages { + var msg ServerMessage + if err := json.Unmarshal(raw, &msg); err != nil { + t.Fatalf("message[%d]: unmarshal: %v", i, err) + } + remarshaled, err := json.Marshal(msg) + if err != nil { + t.Fatalf("message[%d]: re-marshal: %v", i, err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("message[%d]: unmarshal re-marshaled: %v", i, err) + } + if err := json.Unmarshal(raw, &want); err != nil { + t.Fatalf("message[%d]: unmarshal original: %v", i, err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("message[%d]: round-trip mismatch", i) + } + } + }) + } +} diff --git a/agent_sdks/go/a2ui/v09/doc.go b/agent_sdks/go/a2ui/v09/doc.go new file mode 100644 index 0000000000..2858421038 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/doc.go @@ -0,0 +1,2 @@ +// Package v09 provides Go types for the A2UI protocol v0.9. +package v09 diff --git a/agent_sdks/go/a2ui/v09/dynamic.go b/agent_sdks/go/a2ui/v09/dynamic.go new file mode 100644 index 0000000000..fb4c4e3276 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/dynamic.go @@ -0,0 +1,120 @@ +package v09 + +// DynamicString represents a string that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicString struct { + Literal *string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringLiteral creates a DynamicString from a literal string value. +func StringLiteral(s string) DynamicString { return DynamicString{Literal: &s} } + +// StringBinding creates a DynamicString from a data model path. +func StringBinding(path string) DynamicString { + return DynamicString{Binding: &DataBinding{Path: path}} +} + +// StringFunc creates a DynamicString from a function call. +func StringFunc(call FunctionCall) DynamicString { + return DynamicString{FunctionCall: &call} +} + +// DynamicNumber represents a number that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicNumber struct { + Literal *float64 + Binding *DataBinding + FunctionCall *FunctionCall +} + +// NumberLiteral creates a DynamicNumber from a literal float64 value. +func NumberLiteral(n float64) DynamicNumber { return DynamicNumber{Literal: &n} } + +// NumberBinding creates a DynamicNumber from a data model path. +func NumberBinding(path string) DynamicNumber { + return DynamicNumber{Binding: &DataBinding{Path: path}} +} + +// NumberFunc creates a DynamicNumber from a function call. +func NumberFunc(call FunctionCall) DynamicNumber { + return DynamicNumber{FunctionCall: &call} +} + +// DynamicBoolean represents a boolean that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicBoolean struct { + Literal *bool + Binding *DataBinding + FunctionCall *FunctionCall +} + +// BoolLiteral creates a DynamicBoolean from a literal bool value. +func BoolLiteral(b bool) DynamicBoolean { return DynamicBoolean{Literal: &b} } + +// BoolBinding creates a DynamicBoolean from a data model path. +func BoolBinding(path string) DynamicBoolean { + return DynamicBoolean{Binding: &DataBinding{Path: path}} +} + +// BoolFunc creates a DynamicBoolean from a function call. +func BoolFunc(call FunctionCall) DynamicBoolean { + return DynamicBoolean{FunctionCall: &call} +} + +// DynamicStringList represents a string list that can be a literal, a data +// binding, or a function call. Exactly one field is non-nil. +type DynamicStringList struct { + Literal []string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringListLiteral creates a DynamicStringList from literal string values. +func StringListLiteral(ss []string) DynamicStringList { + return DynamicStringList{Literal: ss} +} + +// StringListBinding creates a DynamicStringList from a data model path. +func StringListBinding(path string) DynamicStringList { + return DynamicStringList{Binding: &DataBinding{Path: path}} +} + +// StringListFunc creates a DynamicStringList from a function call. +func StringListFunc(call FunctionCall) DynamicStringList { + return DynamicStringList{FunctionCall: &call} +} + +// DynamicValue represents a value of any type: string, number, boolean, array, +// data binding, or function call. Exactly one field is non-nil. +type DynamicValue struct { + String *string + Number *float64 + Bool *bool + Array []any + Binding *DataBinding + FunctionCall *FunctionCall +} + +// ValueString creates a DynamicValue from a string. +func ValueString(s string) DynamicValue { return DynamicValue{String: &s} } + +// ValueNumber creates a DynamicValue from a number. +func ValueNumber(n float64) DynamicValue { return DynamicValue{Number: &n} } + +// ValueBool creates a DynamicValue from a boolean. +func ValueBool(b bool) DynamicValue { return DynamicValue{Bool: &b} } + +// ValueArray creates a DynamicValue from an array. +func ValueArray(a []any) DynamicValue { return DynamicValue{Array: a} } + +// ValueBinding creates a DynamicValue from a data model path. +func ValueBinding(path string) DynamicValue { + return DynamicValue{Binding: &DataBinding{Path: path}} +} + +// ValueFunc creates a DynamicValue from a function call. +func ValueFunc(call FunctionCall) DynamicValue { + return DynamicValue{FunctionCall: &call} +} diff --git a/agent_sdks/go/a2ui/v09/dynamic_json.go b/agent_sdks/go/a2ui/v09/dynamic_json.go new file mode 100644 index 0000000000..1ec6dff2ce --- /dev/null +++ b/agent_sdks/go/a2ui/v09/dynamic_json.go @@ -0,0 +1,239 @@ +package v09 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for DynamicString. +func (d DynamicString) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicString has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicString has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicString. +func (d *DynamicString) UnmarshalJSON(data []byte) error { + *d = DynamicString{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.Literal = &s + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicNumber. +func (d DynamicNumber) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicNumber has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicNumber has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicNumber. +func (d *DynamicNumber) UnmarshalJSON(data []byte) error { + *d = DynamicNumber{} + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Literal = &n + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicBoolean. +func (d DynamicBoolean) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicBoolean has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicBoolean has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicBoolean. +func (d *DynamicBoolean) UnmarshalJSON(data []byte) error { + *d = DynamicBoolean{} + var b bool + if err := json.Unmarshal(data, &b); err == nil { + d.Literal = &b + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicStringList. +func (d DynamicStringList) MarshalJSON() ([]byte, error) { + if count := countSliceValues(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicStringList has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicStringList has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicStringList. +func (d *DynamicStringList) UnmarshalJSON(data []byte) error { + *d = DynamicStringList{} + var ss []string + if err := json.Unmarshal(data, &ss); err == nil { + d.Literal = ss + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicValue. +func (d DynamicValue) MarshalJSON() ([]byte, error) { + if count := countDynamicValueFields(d); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicValue has multiple values set") + } + switch { + case d.String != nil: + return json.Marshal(*d.String) + case d.Number != nil: + return json.Marshal(*d.Number) + case d.Bool != nil: + return json.Marshal(*d.Bool) + case d.Array != nil: + return json.Marshal(d.Array) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicValue has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicValue. +func (d *DynamicValue) UnmarshalJSON(data []byte) error { + *d = DynamicValue{} + data = bytes.TrimSpace(data) + // Try string. + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.String = &s + return nil + } + // Try bool (before number, since Go's json decoder doesn't confuse them, + // but we check bool first for clarity). + var b bool + if err := json.Unmarshal(data, &b); err == nil { + if len(data) > 0 && (data[0] == 't' || data[0] == 'f') { + d.Bool = &b + return nil + } + } + // Try number. + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Number = &n + return nil + } + // Try array. + var arr []any + if err := json.Unmarshal(data, &arr); err == nil { + d.Array = arr + return nil + } + // Must be an object: binding or function call. + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// unmarshalBindingOrFunc tries to unmarshal data as a DataBinding (has "path" +// key) or a FunctionCall (has "call" key). +func unmarshalBindingOrFunc(data []byte, binding **DataBinding, fn **FunctionCall) error { + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: cannot unmarshal dynamic value: %w", err) + } + if _, ok := obj["path"]; ok { + if _, ok := obj["call"]; ok { + return fmt.Errorf("a2ui: object cannot be both a data binding and a function call") + } + var db DataBinding + if err := json.Unmarshal(data, &db); err != nil { + return fmt.Errorf("a2ui: unmarshal data binding: %w", err) + } + *binding = &db + return nil + } + if _, ok := obj["call"]; ok { + var fc FunctionCall + if err := json.Unmarshal(data, &fc); err != nil { + return fmt.Errorf("a2ui: unmarshal function call: %w", err) + } + *fn = &fc + return nil + } + return fmt.Errorf("a2ui: object is neither a data binding nor a function call") +} + +func countSliceValues(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} + +func countDynamicValueFields(d DynamicValue) int { + var count int + if d.String != nil { + count++ + } + if d.Number != nil { + count++ + } + if d.Bool != nil { + count++ + } + if d.Array != nil { + count++ + } + if d.Binding != nil { + count++ + } + if d.FunctionCall != nil { + count++ + } + return count +} diff --git a/agent_sdks/go/a2ui/v09/dynamic_test.go b/agent_sdks/go/a2ui/v09/dynamic_test.go new file mode 100644 index 0000000000..d338c69c4f --- /dev/null +++ b/agent_sdks/go/a2ui/v09/dynamic_test.go @@ -0,0 +1,308 @@ +package v09 + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestDynamicString(t *testing.T) { + tests := []struct { + name string + json string + want DynamicString + }{ + { + name: "literal", + json: `"hello"`, + want: StringLiteral("hello"), + }, + { + name: "binding", + json: `{"path":"/foo"}`, + want: StringBinding("/foo"), + }, + { + name: "function_call", + json: `{"call":"formatString","args":{"value":"hi"},"returnType":"string"}`, + want: StringFunc(FunctionCall{ + Call: "formatString", + Args: map[string]any{"value": "hi"}, + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicNumber(t *testing.T) { + tests := []struct { + name string + json string + want DynamicNumber + }{ + { + name: "literal", + json: `42.5`, + want: NumberLiteral(42.5), + }, + { + name: "integer", + json: `100`, + want: NumberLiteral(100), + }, + { + name: "binding", + json: `{"path":"/count"}`, + want: NumberBinding("/count"), + }, + { + name: "function_call", + json: `{"call":"add","args":{"a":1,"b":2},"returnType":"number"}`, + want: NumberFunc(FunctionCall{ + Call: "add", + Args: map[string]any{"a": float64(1), "b": float64(2)}, + ReturnType: ReturnTypeNumber, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicNumber + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicBoolean(t *testing.T) { + tests := []struct { + name string + json string + want DynamicBoolean + }{ + { + name: "true", + json: `true`, + want: BoolLiteral(true), + }, + { + name: "false", + json: `false`, + want: BoolLiteral(false), + }, + { + name: "binding", + json: `{"path":"/enabled"}`, + want: BoolBinding("/enabled"), + }, + { + name: "function_call", + json: `{"call":"required","args":{"value":"x"},"returnType":"boolean"}`, + want: BoolFunc(FunctionCall{ + Call: "required", + Args: map[string]any{"value": "x"}, + ReturnType: ReturnTypeBoolean, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicBoolean + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicStringList(t *testing.T) { + tests := []struct { + name string + json string + want DynamicStringList + }{ + { + name: "literal", + json: `["a","b"]`, + want: StringListLiteral([]string{"a", "b"}), + }, + { + name: "binding", + json: `{"path":"/tags"}`, + want: StringListBinding("/tags"), + }, + { + name: "function_call", + json: `{"call":"split","args":{"sep":","},"returnType":"array"}`, + want: StringListFunc(FunctionCall{ + Call: "split", + Args: map[string]any{"sep": ","}, + ReturnType: ReturnTypeArray, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicStringList + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicValue(t *testing.T) { + tests := []struct { + name string + json string + want DynamicValue + }{ + { + name: "string", + json: `"hello"`, + want: ValueString("hello"), + }, + { + name: "number", + json: `3.14`, + want: ValueNumber(3.14), + }, + { + name: "bool_true", + json: `true`, + want: ValueBool(true), + }, + { + name: "bool_false", + json: `false`, + want: ValueBool(false), + }, + { + name: "array", + json: `[1,"two",true]`, + want: ValueArray([]any{float64(1), "two", true}), + }, + { + name: "binding", + json: `{"path":"/data"}`, + want: ValueBinding("/data"), + }, + { + name: "function_call", + json: `{"call":"now","returnType":"string"}`, + want: ValueFunc(FunctionCall{ + Call: "now", + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicValue + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicMarshalEmpty(t *testing.T) { + tests := []struct { + name string + fn func() ([]byte, error) + }{ + {"DynamicString", func() ([]byte, error) { return json.Marshal(DynamicString{}) }}, + {"DynamicNumber", func() ([]byte, error) { return json.Marshal(DynamicNumber{}) }}, + {"DynamicBoolean", func() ([]byte, error) { return json.Marshal(DynamicBoolean{}) }}, + {"DynamicStringList", func() ([]byte, error) { return json.Marshal(DynamicStringList{}) }}, + {"DynamicValue", func() ([]byte, error) { return json.Marshal(DynamicValue{}) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.fn() + if err == nil { + t.Fatal("expected error for zero-value marshal, got nil") + } + }) + } +} + +func TestDynamicRejectsMultipleValues(t *testing.T) { + s := "hello" + tests := []struct { + name string + value any + }{ + {"DynamicString", DynamicString{Literal: &s, Binding: &DataBinding{Path: "/name"}}}, + {"DynamicNumber", DynamicNumber{Literal: float64Ptr(42), Binding: &DataBinding{Path: "/count"}}}, + {"DynamicBoolean", DynamicBoolean{Literal: boolPtr(true), Binding: &DataBinding{Path: "/enabled"}}}, + {"DynamicStringList", DynamicStringList{Literal: []string{"a"}, Binding: &DataBinding{Path: "/tags"}}}, + {"DynamicValue", DynamicValue{String: &s, Binding: &DataBinding{Path: "/value"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.value); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestDynamicRejectsAmbiguousObject(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(`{"path":"/name","call":"formatString"}`), &got); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +// roundTrip marshals v, then verifies the JSON is semantically equivalent to wantJSON. +func roundTrip(t *testing.T, v any, wantJSON string) { + t.Helper() + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal marshaled: %v", err) + } + if err := json.Unmarshal([]byte(wantJSON), &want); err != nil { + t.Fatalf("unmarshal want: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round-trip mismatch:\n got: %s\n want: %s", data, wantJSON) + } +} + +func boolPtr(v bool) *bool { return &v } + +func float64Ptr(v float64) *float64 { return &v } diff --git a/agent_sdks/go/a2ui/v09/example_test.go b/agent_sdks/go/a2ui/v09/example_test.go new file mode 100644 index 0000000000..ccd051d97d --- /dev/null +++ b/agent_sdks/go/a2ui/v09/example_test.go @@ -0,0 +1,63 @@ +package v09_test + +import ( + "encoding/json" + "fmt" + + v09 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v09" +) + +func Example() { + msg := v09.ServerMessage{ + Version: v09.Version, + CreateSurface: &v09.CreateSurface{ + SurfaceID: "demo", + CatalogID: "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + }, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) + // Output: {"version":"v0.9","createSurface":{"surfaceId":"demo","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}} +} + +func ExampleComponent() { + comp := v09.Component{ + ID: "greeting", + Text: &v09.TextComponent{ + Text: v09.StringLiteral("Hello, world!"), + Variant: v09.TextVariantH1, + }, + } + data, _ := json.Marshal(comp) + fmt.Println(string(data)) + // Output: {"component":"Text","id":"greeting","text":"Hello, world!","variant":"h1"} +} + +func ExampleDynamicString() { + // Literal string. + lit := v09.StringLiteral("hello") + data, _ := json.Marshal(lit) + fmt.Println(string(data)) + + // Data binding. + bind := v09.StringBinding("/user/name") + data, _ = json.Marshal(bind) + fmt.Println(string(data)) + // Output: + // "hello" + // {"path":"/user/name"} +} + +func ExampleDynamicNumber() { + n := v09.NumberLiteral(42) + data, _ := json.Marshal(n) + fmt.Println(string(data)) + // Output: 42 +} + +func ExampleDynamicBoolean() { + b := v09.BoolBinding("/settings/enabled") + data, _ := json.Marshal(b) + fmt.Println(string(data)) + // Output: {"path":"/settings/enabled"} +} diff --git a/agent_sdks/go/a2ui/v09/gen.go b/agent_sdks/go/a2ui/v09/gen.go new file mode 100644 index 0000000000..ce7558ad08 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/gen.go @@ -0,0 +1,3 @@ +package v09 + +//go:generate go run ../../cmd/a2uigen -schemas=../../../../specification/v0_9/json -pkg=v09 -stable -out=../.. diff --git a/agent_sdks/go/a2ui/v09/message.go b/agent_sdks/go/a2ui/v09/message.go new file mode 100644 index 0000000000..f2f71e7cf5 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/message.go @@ -0,0 +1,71 @@ +package v09 + +// Version is the A2UI protocol version implemented by this package. +const Version = "v0.9" + +// ServerMessage is a message sent from the agent to the renderer. +// Exactly one of the payload fields is non-nil. +type ServerMessage struct { + Version string `json:"version"` + CreateSurface *CreateSurface `json:"createSurface,omitempty"` + UpdateComponents *UpdateComponents `json:"updateComponents,omitempty"` + UpdateDataModel *UpdateDataModel `json:"updateDataModel,omitempty"` + DeleteSurface *DeleteSurface `json:"deleteSurface,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ServerMessage) VersionString() string { return m.Version } + +// CreateSurface signals the client to create a new surface. +type CreateSurface struct { + SurfaceID string `json:"surfaceId"` + CatalogID string `json:"catalogId"` + Theme *Theme `json:"theme,omitempty"` + SendDataModel bool `json:"sendDataModel,omitempty"` +} + +// UpdateComponents updates a surface with a new set of components. +type UpdateComponents struct { + SurfaceID string `json:"surfaceId"` + Components []Component `json:"components"` +} + +// UpdateDataModel updates the data model for a surface. +type UpdateDataModel struct { + SurfaceID string `json:"surfaceId"` + Path string `json:"path,omitempty"` + Value any `json:"value,omitempty"` +} + +// DeleteSurface signals the client to delete a surface. +type DeleteSurface struct { + SurfaceID string `json:"surfaceId"` +} + +// ClientMessage is a message sent from the renderer to the agent. +// Exactly one of Action or Error is non-nil. +type ClientMessage struct { + Version string `json:"version"` + Action *ActionEvent `json:"action,omitempty"` + Error *ClientError `json:"error,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ClientMessage) VersionString() string { return m.Version } + +// ActionEvent reports a user-initiated action from a component. +type ActionEvent struct { + Name string `json:"name"` + SurfaceID string `json:"surfaceId"` + SourceComponentID string `json:"sourceComponentId"` + Timestamp string `json:"timestamp"` + Context map[string]any `json:"context"` +} + +// ClientError reports a client-side error. +type ClientError struct { + Code string `json:"code"` + SurfaceID string `json:"surfaceId"` + Message string `json:"message"` + Path string `json:"path,omitempty"` +} diff --git a/agent_sdks/go/a2ui/v09/message_json.go b/agent_sdks/go/a2ui/v09/message_json.go new file mode 100644 index 0000000000..0c9d98ad76 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/message_json.go @@ -0,0 +1,76 @@ +package v09 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for ServerMessage. +func (m ServerMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ServerMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ServerMessage. +func (m *ServerMessage) UnmarshalJSON(data []byte) error { + type alias ServerMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal server message: %w", err) + } + msg := ServerMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ServerMessage) validate() error { + switch countSet(m.CreateSurface != nil, m.UpdateComponents != nil, m.UpdateDataModel != nil, m.DeleteSurface != nil) { + case 1: + return nil + case 0: + return fmt.Errorf("a2ui: server message has no payload set") + default: + return fmt.Errorf("a2ui: server message has multiple payloads set") + } +} + +// MarshalJSON implements json.Marshaler for ClientMessage. +func (m ClientMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ClientMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ClientMessage. +func (m *ClientMessage) UnmarshalJSON(data []byte) error { + type alias ClientMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal client message: %w", err) + } + msg := ClientMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ClientMessage) validate() error { + switch countSet(m.Action != nil, m.Error != nil) { + case 1: + return nil + case 0: + return fmt.Errorf("a2ui: client message has no payload set") + default: + return fmt.Errorf("a2ui: client message has multiple payloads set") + } +} diff --git a/agent_sdks/go/a2ui/v09/message_test.go b/agent_sdks/go/a2ui/v09/message_test.go new file mode 100644 index 0000000000..8ee8e4eb8b --- /dev/null +++ b/agent_sdks/go/a2ui/v09/message_test.go @@ -0,0 +1,320 @@ +package v09 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +func TestFlightStatusMessages(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/01_flight-status.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Name string `json:"name"` + Description string `json:"description"` + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + if len(example.Messages) != 3 { + t.Fatalf("got %d messages, want 3", len(example.Messages)) + } + + tests := []struct { + name string + index int + wantField string + }{ + {"CreateSurface", 0, "createSurface"}, + {"UpdateComponents", 1, "updateComponents"}, + {"UpdateDataModel", 2, "updateDataModel"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var msg ServerMessage + if err := json.Unmarshal(example.Messages[tt.index], &msg); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if msg.Version != Version { + t.Fatalf("version = %q, want %q", msg.Version, Version) + } + switch tt.wantField { + case "createSurface": + if msg.CreateSurface == nil { + t.Fatal("CreateSurface is nil") + } + if msg.CreateSurface.SurfaceID != "gallery-flight-status" { + t.Fatalf("surfaceId = %q", msg.CreateSurface.SurfaceID) + } + case "updateComponents": + if msg.UpdateComponents == nil { + t.Fatal("UpdateComponents is nil") + } + if len(msg.UpdateComponents.Components) == 0 { + t.Fatal("no components") + } + case "updateDataModel": + if msg.UpdateDataModel == nil { + t.Fatal("UpdateDataModel is nil") + } + } + + // Round-trip: marshal and compare JSON equivalence. + jsonEquivalent(t, example.Messages[tt.index], msg) + }) + } +} + +func TestServerMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "create_surface", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{ + SurfaceID: "test-1", + CatalogID: "https://example.com/catalog.json", + }, + }, + }, + { + name: "delete_surface", + msg: ServerMessage{ + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "test-1"}, + }, + }, + { + name: "update_data_model", + msg: ServerMessage{ + Version: Version, + UpdateDataModel: &UpdateDataModel{ + SurfaceID: "test-1", + Path: "/count", + Value: float64(42), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestClientMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "action", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{ + Name: "submit", + SurfaceID: "test-1", + SourceComponentID: "btn-1", + Timestamp: "2025-01-01T00:00:00Z", + Context: map[string]any{"key": "value"}, + }, + }, + }, + { + name: "error", + msg: ClientMessage{ + Version: Version, + Error: &ClientError{ + Code: "INVALID_SURFACE", + SurfaceID: "test-1", + Message: "surface not found", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ClientMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestServerMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "none", + msg: ServerMessage{Version: Version}, + }, + { + name: "multiple", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.9"}`, + `{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"cat"},"deleteSurface":{"surfaceId":"s1"}}`, + } { + var msg ServerMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestClientMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "none", + msg: ClientMessage{Version: Version}, + }, + { + name: "multiple", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{Name: "submit", SurfaceID: "s1", SourceComponentID: "btn", Timestamp: "2025-01-01T00:00:00Z"}, + Error: &ClientError{Code: "ERR", SurfaceID: "s1", Message: "bad"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.9"}`, + `{"version":"v0.9","action":{"name":"submit","surfaceId":"s1","sourceComponentId":"btn","timestamp":"2025-01-01T00:00:00Z"},"error":{"code":"ERR","surfaceId":"s1","message":"bad"}}`, + } { + var msg ClientMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestServerMessageListWrapper(t *testing.T) { + wrapper := ServerMessageListWrapper{ + Messages: []ServerMessage{ + { + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + }, + { + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + data, err := json.Marshal(wrapper) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessageListWrapper + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, wrapper) { + t.Fatalf("round-trip mismatch\n got: %+v\n want: %+v", got, wrapper) + } +} + +func TestClientMessageListWrapperEmpty(t *testing.T) { + data := []byte(`{"messages":[]}`) + var w ClientMessageListWrapper + if err := json.Unmarshal(data, &w); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(w.Messages) != 0 { + t.Fatalf("got %d messages, want 0", len(w.Messages)) + } +} + +// jsonEquivalent marshals v and checks that the result is semantically +// equivalent to the original JSON. Empty maps/objects and missing fields +// are treated as equivalent (omitempty normalization). +func jsonEquivalent(t *testing.T, original json.RawMessage, v any) { + t.Helper() + remarshaled, err := json.Marshal(v) + if err != nil { + t.Fatalf("re-marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("unmarshal re-marshaled: %v", err) + } + if err := json.Unmarshal(original, &want); err != nil { + t.Fatalf("unmarshal original: %v", err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Fatalf("JSON not equivalent:\n got: %s\n want: %s", remarshaled, original) + } +} + +// normalizeJSON removes empty maps and nil values in-place so that +// omitempty differences don't cause false mismatches. +func normalizeJSON(v any) { + switch v := v.(type) { + case map[string]any: + for k, val := range v { + normalizeJSON(val) + // Remove keys whose value is an empty map (matches omitempty behavior). + if m, ok := val.(map[string]any); ok && len(m) == 0 { + delete(v, k) + } + } + case []any: + for _, elem := range v { + normalizeJSON(elem) + } + } +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/01_flight-status.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/01_flight-status.json new file mode 100644 index 0000000000..aef954225c --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/01_flight-status.json @@ -0,0 +1,201 @@ +{ + "name": "Flight Status", + "description": "Example of flight status demonstrating date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-flight-status", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-flight-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "route-row", "divider", "times-row"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-left", "date"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "header-left", + "component": "Row", + "children": ["flight-indicator", "flight-number"], + "align": "center" + }, + { + "id": "flight-indicator", + "component": "Icon", + "name": "send" + }, + { + "id": "flight-number", + "component": "Text", + "text": { + "path": "/flightNumber" + }, + "variant": "h3" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "E, MMM d" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "route-row", + "component": "Row", + "children": ["origin", "arrow", "destination"], + "align": "center" + }, + { + "id": "origin", + "component": "Text", + "text": { + "path": "/origin" + }, + "variant": "h2" + }, + { + "id": "arrow", + "component": "Text", + "text": "\u2192", + "variant": "h2" + }, + { + "id": "destination", + "component": "Text", + "text": { + "path": "/destination" + }, + "variant": "h2" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "times-row", + "component": "Row", + "children": ["departure-col", "status-col", "arrival-col"], + "justify": "spaceBetween" + }, + { + "id": "departure-col", + "component": "Column", + "children": ["departure-label", "departure-time"], + "align": "start" + }, + { + "id": "departure-label", + "component": "Text", + "text": "Departs", + "variant": "caption" + }, + { + "id": "departure-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/departureTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "status-col", + "component": "Column", + "children": ["status-label", "status-value"], + "align": "center" + }, + { + "id": "status-label", + "component": "Text", + "text": "Status", + "variant": "caption" + }, + { + "id": "status-value", + "component": "Text", + "text": { + "path": "/status" + }, + "variant": "body" + }, + { + "id": "arrival-col", + "component": "Column", + "children": ["arrival-label", "arrival-time"], + "align": "end" + }, + { + "id": "arrival-label", + "component": "Text", + "text": "Arrives", + "variant": "caption" + }, + { + "id": "arrival-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/arrivalTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-flight-status", + "value": { + "flightNumber": "OS 87", + "date": "2025-12-15", + "origin": "Vienna", + "destination": "New York", + "departureTime": "2025-12-15T10:15:00Z", + "status": "On Time", + "arrivalTime": "2025-12-15T14:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/02_email-compose.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/02_email-compose.json new file mode 100644 index 0000000000..4eb081bc2e --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/02_email-compose.json @@ -0,0 +1,185 @@ +{ + "name": "Email Compose", + "description": "Example of email compose", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-email-compose", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-email-compose", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["from-row", "to-row", "subject-row", "divider", "message", "actions"] + }, + { + "id": "from-row", + "component": "Row", + "children": ["from-label", "from-value"], + "align": "center" + }, + { + "id": "from-label", + "component": "Text", + "text": "FROM", + "variant": "caption" + }, + { + "id": "from-value", + "component": "Text", + "text": { + "path": "/from" + }, + "variant": "body" + }, + { + "id": "to-row", + "component": "Row", + "children": ["to-label", "to-value"], + "align": "center" + }, + { + "id": "to-label", + "component": "Text", + "text": "TO", + "variant": "caption" + }, + { + "id": "to-value", + "component": "Text", + "text": { + "path": "/to" + }, + "variant": "body" + }, + { + "id": "subject-row", + "component": "Row", + "children": ["subject-label", "subject-value"], + "align": "center" + }, + { + "id": "subject-label", + "component": "Text", + "text": "SUBJECT", + "variant": "caption" + }, + { + "id": "subject-value", + "component": "Text", + "text": { + "path": "/subject" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "message", + "component": "Column", + "children": ["greeting", "body-text", "closing", "signature"] + }, + { + "id": "greeting", + "component": "Text", + "text": { + "path": "/greeting" + }, + "variant": "body" + }, + { + "id": "body-text", + "component": "Text", + "text": { + "path": "/body" + }, + "variant": "body" + }, + { + "id": "closing", + "component": "Text", + "text": { + "path": "/closing" + }, + "variant": "body" + }, + { + "id": "signature", + "component": "Text", + "text": { + "path": "/signature" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["send-btn", "discard-btn"] + }, + { + "id": "send-btn-text", + "component": "Text", + "text": "Send email" + }, + { + "id": "send-btn", + "component": "Button", + "child": "send-btn-text", + "action": { + "event": { + "name": "send", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-email-compose", + "value": { + "from": "alex@acme.com", + "to": "jordan@acme.com", + "subject": "Q4 Revenue Forecast", + "greeting": "Hi Jordan,", + "body": "Following up on our call. Please review the attached Q4 forecast and let me know if you have questions before the board meeting.", + "closing": "Best,", + "signature": "Alex" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/03_calendar-day.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/03_calendar-day.json new file mode 100644 index 0000000000..301c3d1dd4 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/03_calendar-day.json @@ -0,0 +1,166 @@ +{ + "name": "Calendar Day", + "description": "Example of calendar day demonstrating dynamic templating, relative paths, and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-calendar-day", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-calendar-day", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "divider", "actions"] + }, + { + "id": "header-row", + "component": "Row", + "children": ["date-col", "events-col"] + }, + { + "id": "date-col", + "component": "Column", + "children": ["day-name", "day-number"], + "align": "start" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-number", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "d" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "events-col", + "component": "Column", + "children": { + "path": "/events", + "componentId": "event-template" + } + }, + { + "id": "event-template", + "component": "Column", + "children": ["event-title", "event-time"] + }, + { + "id": "event-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "event-time", + "component": "Text", + "text": { + "path": "time" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-btn", "discard-btn"] + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to calendar" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-calendar-day", + "value": { + "date": "2025-12-28", + "events": [ + { + "title": "Lunch", + "time": "12:00 - 12:45 PM" + }, + { + "title": "Q1 roadmap review", + "time": "1:00 - 2:00 PM" + }, + { + "title": "Team standup", + "time": "3:30 - 4:00 PM" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/04_weather-current.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/04_weather-current.json new file mode 100644 index 0000000000..a2e1ebd5f7 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/04_weather-current.json @@ -0,0 +1,168 @@ +{ + "name": "Weather Current", + "description": "Example of weather current demonstrating templating and string formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-weather-current", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-weather-current", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["temp-row", "location", "description", "forecast-row"], + "align": "center" + }, + { + "id": "temp-row", + "component": "Row", + "children": ["temp-high", "temp-low"], + "align": "start" + }, + { + "id": "temp-high", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempHigh}°" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "temp-low", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempLow}°" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "location", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "caption" + }, + { + "id": "forecast-row", + "component": "Row", + "children": { + "path": "/forecast", + "componentId": "forecast-day-template" + }, + "justify": "spaceAround" + }, + { + "id": "forecast-day-template", + "component": "Column", + "children": ["day-name", "day-icon", "day-temp"], + "align": "center" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "date" + }, + "format": "E" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-icon", + "component": "Text", + "text": { + "path": "icon" + }, + "variant": "h3" + }, + { + "id": "day-temp", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${temp}°" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-weather-current", + "value": { + "tempHigh": 72, + "tempLow": 58, + "location": "Austin, TX", + "description": "Clear skies with light breeze", + "forecast": [ + { + "date": "2025-12-16", + "icon": "☀️", + "temp": 74 + }, + { + "date": "2025-12-17", + "icon": "☀️", + "temp": 76 + }, + { + "date": "2025-12-18", + "icon": "⛅", + "temp": 71 + }, + { + "date": "2025-12-19", + "icon": "☀️", + "temp": 73 + }, + { + "date": "2025-12-20", + "icon": "☀️", + "temp": 75 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/05_product-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/05_product-card.json new file mode 100644 index 0000000000..ee6634062b --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/05_product-card.json @@ -0,0 +1,151 @@ +{ + "name": "Product Card", + "description": "Example of product card demonstrating currency formatting and pluralization.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-product-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-product-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["image", "details"] + }, + { + "id": "image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "details", + "component": "Column", + "children": ["name", "rating-row", "price-row", "actions"] + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["stars", "reviews"], + "align": "center" + }, + { + "id": "stars", + "component": "Text", + "text": { + "path": "/stars" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "price-row", + "component": "Row", + "children": ["price", "original-price"], + "align": "start" + }, + { + "id": "price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "original-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/originalPrice" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-cart-btn"] + }, + { + "id": "add-cart-btn-text", + "component": "Text", + "text": "Add to Cart" + }, + { + "id": "add-cart-btn", + "component": "Button", + "child": "add-cart-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "addToCart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-product-card", + "value": { + "imageUrl": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&h=200&fit=crop", + "name": "Wireless Headphones Pro", + "stars": "★★★★★", + "reviewCount": 2847, + "price": 199.99, + "originalPrice": 249.99 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/06_music-player.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/06_music-player.json new file mode 100644 index 0000000000..249e17cce4 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/06_music-player.json @@ -0,0 +1,165 @@ +{ + "name": "Music Player", + "description": "Example of music player", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-music-player", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-music-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["album-art", "track-info", "progress", "time-row", "controls"], + "align": "center" + }, + { + "id": "album-art", + "component": "Image", + "url": { + "path": "/albumArt" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["song-title", "artist"], + "align": "center" + }, + { + "id": "song-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "artist", + "component": "Text", + "text": { + "path": "/artist" + }, + "variant": "caption" + }, + { + "id": "progress", + "component": "Slider", + "value": { + "path": "/progress" + }, + "max": 1 + }, + { + "id": "time-row", + "component": "Row", + "children": ["current-time", "total-time"], + "justify": "spaceBetween" + }, + { + "id": "current-time", + "component": "Text", + "text": { + "path": "/currentTime" + }, + "variant": "caption" + }, + { + "id": "total-time", + "component": "Text", + "text": { + "path": "/totalTime" + }, + "variant": "caption" + }, + { + "id": "controls", + "component": "Row", + "children": ["prev-btn", "play-btn", "next-btn"], + "justify": "center" + }, + { + "id": "prev-btn-icon", + "component": "Icon", + "name": "skipPrevious" + }, + { + "id": "prev-btn", + "component": "Button", + "child": "prev-btn-icon", + "action": { + "event": { + "name": "previous", + "context": {} + } + } + }, + { + "id": "play-btn-icon", + "component": "Icon", + "name": { + "path": "/playIcon" + } + }, + { + "id": "play-btn", + "component": "Button", + "child": "play-btn-icon", + "action": { + "event": { + "name": "playPause", + "context": {} + } + } + }, + { + "id": "next-btn-icon", + "component": "Icon", + "name": "skipNext" + }, + { + "id": "next-btn", + "component": "Button", + "child": "next-btn-icon", + "action": { + "event": { + "name": "next", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-music-player", + "value": { + "albumArt": "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=300&h=300&fit=crop", + "title": "Blinding Lights", + "artist": "The Weeknd", + "album": "After Hours", + "progress": 0.45, + "currentTime": "1:48", + "totalTime": "4:22", + "playIcon": "pause" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/07_task-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/07_task-card.json new file mode 100644 index 0000000000..9b6940318b --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/07_task-card.json @@ -0,0 +1,107 @@ +{ + "name": "Task Card", + "description": "Example of task card demonstrating CheckBox and DateTimeInput.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-task-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-task-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["status-checkbox", "content", "priority"], + "align": "start" + }, + { + "id": "status-checkbox", + "component": "CheckBox", + "label": "", + "value": { + "path": "/completed" + } + }, + { + "id": "content", + "component": "Column", + "children": ["title", "description", "meta-row"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["due-date-input", "project"], + "align": "center" + }, + { + "id": "due-date-input", + "component": "DateTimeInput", + "label": "Due", + "value": { + "path": "/dueDate" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "project", + "component": "Text", + "text": { + "path": "/project" + }, + "variant": "caption" + }, + { + "id": "priority", + "component": "Icon", + "name": { + "path": "/priorityIcon" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-task-card", + "value": { + "completed": false, + "title": "Review pull request", + "description": "Review and approve the authentication module changes.", + "dueDate": "2025-12-15T17:00:00Z", + "project": "Backend", + "priorityIcon": "priority_high" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/08_user-profile.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/08_user-profile.json new file mode 100644 index 0000000000..ecec1ad7ec --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/08_user-profile.json @@ -0,0 +1,190 @@ +{ + "name": "User Profile", + "description": "Example of user profile demonstrating number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-user-profile", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-user-profile", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "info", "bio", "stats-row", "follow-btn"], + "align": "center" + }, + { + "id": "header", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "info", + "component": "Column", + "children": ["name", "username"], + "align": "center" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "username", + "component": "Text", + "text": { + "path": "/username" + }, + "variant": "caption" + }, + { + "id": "bio", + "component": "Text", + "text": { + "path": "/bio" + }, + "variant": "body" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["followers-col", "following-col", "posts-col"], + "justify": "spaceAround" + }, + { + "id": "followers-col", + "component": "Column", + "children": ["followers-count", "followers-label"], + "align": "center" + }, + { + "id": "followers-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/followers" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "followers-label", + "component": "Text", + "text": "Followers", + "variant": "caption" + }, + { + "id": "following-col", + "component": "Column", + "children": ["following-count", "following-label"], + "align": "center" + }, + { + "id": "following-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/following" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "following-label", + "component": "Text", + "text": "Following", + "variant": "caption" + }, + { + "id": "posts-col", + "component": "Column", + "children": ["posts-count", "posts-label"], + "align": "center" + }, + { + "id": "posts-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/posts" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "posts-label", + "component": "Text", + "text": "Posts", + "variant": "caption" + }, + { + "id": "follow-btn-text", + "component": "Text", + "text": { + "path": "/followText" + } + }, + { + "id": "follow-btn", + "component": "Button", + "child": "follow-btn-text", + "action": { + "event": { + "name": "follow", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-user-profile", + "value": { + "avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop", + "name": "Sarah Chen", + "username": "@sarahchen", + "bio": "Product Designer at Tech Co. Creating delightful experiences.", + "followers": 12400, + "following": 892, + "posts": 347, + "followText": "Follow" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/09_login-form.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/09_login-form.json new file mode 100644 index 0000000000..8c542e6791 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/09_login-form.json @@ -0,0 +1,214 @@ +{ + "name": "Login Form with Validation", + "description": "Example of login form demonstrating validation checks and logic.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-login-form", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-login-form", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header", + "email-field", + "password-field", + "login-btn", + "divider", + "signup-text" + ] + }, + { + "id": "header", + "component": "Column", + "children": ["title", "subtitle"], + "align": "center" + }, + { + "id": "title", + "component": "Text", + "text": "Welcome back", + "variant": "h2" + }, + { + "id": "subtitle", + "component": "Text", + "text": "Sign in to your account", + "variant": "caption" + }, + { + "id": "email-field", + "component": "TextField", + "value": { + "path": "/email" + }, + "label": "Email", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Email is required" + }, + { + "condition": { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Please enter a valid email address" + } + ] + }, + { + "id": "password-field", + "component": "TextField", + "value": { + "path": "/password" + }, + "label": "Password", + "variant": "obscured", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/password" + } + } + }, + "message": "Password is required" + }, + { + "condition": { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + }, + "message": "Password must be at least 8 characters long" + } + ] + }, + { + "id": "login-btn-text", + "component": "Text", + "text": "Sign in" + }, + { + "id": "login-btn", + "component": "Button", + "child": "login-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + } + ] + } + }, + "message": "Please fix errors before signing in" + } + ], + "action": { + "event": { + "name": "login", + "context": { + "email": { + "path": "/email" + } + } + } + } + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "signup-text", + "component": "Row", + "children": ["no-account", "signup-link"], + "justify": "center" + }, + { + "id": "no-account", + "component": "Text", + "text": "Don't have an account?", + "variant": "caption" + }, + { + "id": "signup-link-text", + "component": "Text", + "text": "Sign up" + }, + { + "id": "signup-link", + "component": "Button", + "child": "signup-link-text", + "action": { + "event": { + "name": "signup", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-login-form", + "value": { + "email": "", + "password": "" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/10_notification-permission.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/10_notification-permission.json new file mode 100644 index 0000000000..956cd8fe04 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/10_notification-permission.json @@ -0,0 +1,105 @@ +{ + "name": "Notification Permission", + "description": "Example of notification permission", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-notification-permission", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-notification-permission", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["icon", "title", "description", "actions"], + "align": "center" + }, + { + "id": "icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["yes-btn", "no-btn"], + "justify": "center" + }, + { + "id": "yes-btn-text", + "component": "Text", + "text": "Yes" + }, + { + "id": "yes-btn", + "component": "Button", + "child": "yes-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "no-btn-text", + "component": "Text", + "text": "No" + }, + { + "id": "no-btn", + "component": "Button", + "child": "no-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-notification-permission", + "value": { + "icon": "check", + "title": "Enable notification", + "description": "Get alerts for order status changes" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/11_purchase-complete.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/11_purchase-complete.json new file mode 100644 index 0000000000..9d27d4e204 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/11_purchase-complete.json @@ -0,0 +1,169 @@ +{ + "name": "Purchase Complete", + "description": "Example of purchase complete demonstrating currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-purchase-complete", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-purchase-complete", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "success-icon", + "title", + "product-row", + "divider", + "details-col", + "view-btn" + ], + "align": "center" + }, + { + "id": "success-icon", + "component": "Icon", + "name": "check" + }, + { + "id": "title", + "component": "Text", + "text": "Purchase Complete", + "variant": "h2" + }, + { + "id": "product-row", + "component": "Row", + "children": ["product-image", "product-info"], + "align": "center" + }, + { + "id": "product-image", + "component": "Image", + "url": { + "path": "/productImage" + }, + "fit": "cover" + }, + { + "id": "product-info", + "component": "Column", + "children": ["product-name", "product-price"] + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h4" + }, + { + "id": "product-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "details-col", + "component": "Column", + "children": ["delivery-row", "seller-row"] + }, + { + "id": "delivery-row", + "component": "Row", + "children": ["delivery-icon", "delivery-text"], + "align": "center" + }, + { + "id": "delivery-icon", + "component": "Icon", + "name": "arrowForward" + }, + { + "id": "delivery-text", + "component": "Text", + "text": { + "path": "/deliveryDate" + }, + "variant": "body" + }, + { + "id": "seller-row", + "component": "Row", + "children": ["seller-label", "seller-name"] + }, + { + "id": "seller-label", + "component": "Text", + "text": "Sold by:", + "variant": "caption" + }, + { + "id": "seller-name", + "component": "Text", + "text": { + "path": "/seller" + }, + "variant": "body" + }, + { + "id": "view-btn-text", + "component": "Text", + "text": "View Order Details" + }, + { + "id": "view-btn", + "component": "Button", + "child": "view-btn-text", + "action": { + "event": { + "name": "view_details", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-purchase-complete", + "value": { + "productImage": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&h=100&fit=crop", + "productName": "Wireless Headphones Pro", + "price": 199.99, + "deliveryDate": "Arrives Dec 18 - Dec 20", + "seller": "TechStore Official" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/12_chat-message.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/12_chat-message.json new file mode 100644 index 0000000000..483e11a4a3 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/12_chat-message.json @@ -0,0 +1,144 @@ +{ + "name": "Chat Message", + "description": "Example of chat message demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-chat-message", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-chat-message", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "messages-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["channel-icon", "channel-name"], + "align": "center" + }, + { + "id": "channel-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "channel-name", + "component": "Text", + "text": { + "path": "/channelName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "messages-list", + "component": "Column", + "children": { + "path": "/messages", + "componentId": "message-template" + }, + "align": "start" + }, + { + "id": "message-template", + "component": "Row", + "children": ["msg-avatar", "msg-content"], + "align": "start" + }, + { + "id": "msg-avatar", + "component": "Image", + "url": { + "path": "avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "msg-content", + "component": "Column", + "children": ["msg-header", "msg-text"] + }, + { + "id": "msg-header", + "component": "Row", + "children": ["msg-username", "msg-time"], + "align": "center" + }, + { + "id": "msg-username", + "component": "Text", + "text": { + "path": "username" + }, + "variant": "h4" + }, + { + "id": "msg-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "timestamp" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "msg-text", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-chat-message", + "value": { + "channelName": "project-updates", + "messages": [ + { + "avatar": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop", + "username": "Mike Chen", + "timestamp": "2025-12-15T10:32:00Z", + "text": "Just pushed the new API changes. Ready for review." + }, + { + "avatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=40&h=40&fit=crop", + "username": "Sarah Kim", + "timestamp": "2025-12-15T10:45:00Z", + "text": "Great! I'll take a look after standup." + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/13_coffee-order.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/13_coffee-order.json new file mode 100644 index 0000000000..86ca714ef0 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/13_coffee-order.json @@ -0,0 +1,253 @@ +{ + "name": "Coffee Order", + "description": "Example of coffee order demonstrating templating and currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-coffee-order", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-coffee-order", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "items-list", "divider", "totals", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["coffee-icon", "store-name"], + "align": "center" + }, + { + "id": "coffee-icon", + "component": "Icon", + "name": "favorite" + }, + { + "id": "store-name", + "component": "Text", + "text": { + "path": "/storeName" + }, + "variant": "h3" + }, + { + "id": "items-list", + "component": "Column", + "children": { + "path": "/items", + "componentId": "order-item-template" + } + }, + { + "id": "order-item-template", + "component": "Row", + "children": ["item-details", "item-price"], + "justify": "spaceBetween", + "align": "start" + }, + { + "id": "item-details", + "component": "Column", + "children": ["item-name", "item-size"] + }, + { + "id": "item-name", + "component": "Text", + "text": { + "path": "name" + }, + "variant": "body" + }, + { + "id": "item-size", + "component": "Text", + "text": { + "path": "size" + }, + "variant": "caption" + }, + { + "id": "item-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "totals", + "component": "Column", + "children": ["subtotal-row", "tax-row", "total-row"] + }, + { + "id": "subtotal-row", + "component": "Row", + "children": ["subtotal-label", "subtotal-value"], + "justify": "spaceBetween" + }, + { + "id": "subtotal-label", + "component": "Text", + "text": "Subtotal", + "variant": "caption" + }, + { + "id": "subtotal-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/subtotal" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "tax-row", + "component": "Row", + "children": ["tax-label", "tax-value"], + "justify": "spaceBetween" + }, + { + "id": "tax-label", + "component": "Text", + "text": "Tax", + "variant": "caption" + }, + { + "id": "tax-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/tax" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/total" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h4" + }, + { + "id": "actions", + "component": "Row", + "children": ["purchase-btn", "add-btn"] + }, + { + "id": "purchase-btn-text", + "component": "Text", + "text": "Purchase" + }, + { + "id": "purchase-btn", + "component": "Button", + "child": "purchase-btn-text", + "action": { + "event": { + "name": "purchase", + "context": {} + } + } + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to cart" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add_to_cart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-coffee-order", + "value": { + "storeName": "Sunrise Coffee", + "items": [ + { + "name": "Oat Milk Latte", + "size": "Grande, Extra Shot", + "price": 6.45 + }, + { + "name": "Chocolate Croissant", + "size": "Warmed", + "price": 4.25 + } + ], + "subtotal": 10.7, + "tax": 0.96, + "total": 11.66 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/14_sports-player.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/14_sports-player.json new file mode 100644 index 0000000000..98e48f6a7c --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/14_sports-player.json @@ -0,0 +1,177 @@ +{ + "name": "Sports Player", + "description": "Example of sports player", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-sports-player", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-sports-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["player-image", "player-info", "divider", "stats-row"], + "align": "center" + }, + { + "id": "player-image", + "component": "Image", + "url": { + "path": "/playerImage" + }, + "fit": "cover" + }, + { + "id": "player-info", + "component": "Column", + "children": ["player-name", "player-details"], + "align": "center" + }, + { + "id": "player-name", + "component": "Text", + "text": { + "path": "/playerName" + }, + "variant": "h2" + }, + { + "id": "player-details", + "component": "Row", + "children": ["player-number", "player-team"], + "align": "center" + }, + { + "id": "player-number", + "component": "Text", + "text": { + "path": "/number" + }, + "variant": "h3" + }, + { + "id": "player-team", + "component": "Text", + "text": { + "path": "/team" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["stat1", "stat2", "stat3"], + "justify": "spaceAround" + }, + { + "id": "stat1", + "component": "Column", + "children": ["stat1-value", "stat1-label"], + "align": "center" + }, + { + "id": "stat1-value", + "component": "Text", + "text": { + "path": "/stat1/value" + }, + "variant": "h3" + }, + { + "id": "stat1-label", + "component": "Text", + "text": { + "path": "/stat1/label" + }, + "variant": "caption" + }, + { + "id": "stat2", + "component": "Column", + "children": ["stat2-value", "stat2-label"], + "align": "center" + }, + { + "id": "stat2-value", + "component": "Text", + "text": { + "path": "/stat2/value" + }, + "variant": "h3" + }, + { + "id": "stat2-label", + "component": "Text", + "text": { + "path": "/stat2/label" + }, + "variant": "caption" + }, + { + "id": "stat3", + "component": "Column", + "children": ["stat3-value", "stat3-label"], + "align": "center" + }, + { + "id": "stat3-value", + "component": "Text", + "text": { + "path": "/stat3/value" + }, + "variant": "h3" + }, + { + "id": "stat3-label", + "component": "Text", + "text": { + "path": "/stat3/label" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-sports-player", + "value": { + "playerImage": "https://images.unsplash.com/photo-1546519638-68e109498ffc?w=200&h=200&fit=crop", + "playerName": "Marcus Johnson", + "number": "#23", + "team": "LA Lakers", + "stat1": { + "value": "28.4", + "label": "PPG" + }, + "stat2": { + "value": "7.2", + "label": "RPG" + }, + "stat3": { + "value": "6.8", + "label": "APG" + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/15_account-balance.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/15_account-balance.json new file mode 100644 index 0000000000..96bdd62934 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/15_account-balance.json @@ -0,0 +1,126 @@ +{ + "name": "Account Balance", + "description": "Example of account balance demonstrating currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-account-balance", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-account-balance", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "balance", "updated", "divider", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["account-icon", "account-name"], + "align": "center" + }, + { + "id": "account-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "account-name", + "component": "Text", + "text": { + "path": "/accountName" + }, + "variant": "h4" + }, + { + "id": "balance", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/balance" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "updated", + "component": "Text", + "text": { + "path": "/lastUpdated" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["transfer-btn", "pay-btn"] + }, + { + "id": "transfer-btn-text", + "component": "Text", + "text": "Transfer" + }, + { + "id": "transfer-btn", + "component": "Button", + "child": "transfer-btn-text", + "action": { + "event": { + "name": "transfer", + "context": {} + } + } + }, + { + "id": "pay-btn-text", + "component": "Text", + "text": "Pay Bill" + }, + { + "id": "pay-btn", + "component": "Button", + "child": "pay-btn-text", + "action": { + "event": { + "name": "pay_bill", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-account-balance", + "value": { + "accountName": "Primary Checking", + "balance": 12458.32, + "lastUpdated": "Updated just now" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/16_workout-summary.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/16_workout-summary.json new file mode 100644 index 0000000000..6b43b183ce --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/16_workout-summary.json @@ -0,0 +1,160 @@ +{ + "name": "Workout Summary", + "description": "Example of workout summary demonstrating number and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-workout-summary", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-workout-summary", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "metrics-row", "date"] + }, + { + "id": "header", + "component": "Row", + "children": ["workout-icon", "title"], + "align": "center" + }, + { + "id": "workout-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": "Workout Complete", + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "metrics-row", + "component": "Row", + "children": ["duration-col", "calories-col", "distance-col"], + "justify": "spaceAround" + }, + { + "id": "duration-col", + "component": "Column", + "children": ["duration-value", "duration-label"], + "align": "center" + }, + { + "id": "duration-value", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "h3" + }, + { + "id": "duration-label", + "component": "Text", + "text": "Duration", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} km" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE, MMM d 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-workout-summary", + "value": { + "icon": "directions_run", + "workoutType": "Morning Run", + "duration": "32:15", + "calories": 385, + "distance": 5.2, + "date": "2025-12-15T07:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/17_event-detail.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/17_event-detail.json new file mode 100644 index 0000000000..27264335c4 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/17_event-detail.json @@ -0,0 +1,144 @@ +{ + "name": "Event Detail", + "description": "Example of event detail demonstrating date and string formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-event-detail", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-event-detail", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title", "time-row", "location-row", "description", "divider", "actions"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h2" + }, + { + "id": "time-row", + "component": "Row", + "children": ["time-icon", "time-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "time-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatDate(value: ${/start}, format: 'E, MMM d')} • ${formatDate(value: ${/start}, format: 'h:mm a')} - ${formatDate(value: ${/end}, format: 'h:mm a')}" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["accept-btn", "decline-btn"] + }, + { + "id": "accept-btn-text", + "component": "Text", + "text": "Accept" + }, + { + "id": "accept-btn", + "component": "Button", + "child": "accept-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "decline-btn-text", + "component": "Text", + "text": "Decline" + }, + { + "id": "decline-btn", + "component": "Button", + "child": "decline-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-event-detail", + "value": { + "title": "Product Launch Meeting", + "start": "2025-12-19T14:00:00Z", + "end": "2025-12-19T15:30:00Z", + "location": "Conference Room A, Building 2", + "description": "Review final product specs and marketing materials before the Q1 launch." + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/18_track-list.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/18_track-list.json new file mode 100644 index 0000000000..2db21c4ade --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/18_track-list.json @@ -0,0 +1,152 @@ +{ + "name": "Track List", + "description": "Example of track list demonstrating templating, relative paths, and number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-track-list", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-track-list", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "tracks-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["playlist-icon", "playlist-name"], + "align": "center" + }, + { + "id": "playlist-icon", + "component": "Icon", + "name": "play" + }, + { + "id": "playlist-name", + "component": "Text", + "text": { + "path": "/playlistName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "tracks-list", + "component": "Column", + "children": { + "path": "/tracks", + "componentId": "track-item-template" + } + }, + { + "id": "track-item-template", + "component": "Row", + "children": ["track-num", "track-art", "track-info", "track-duration"], + "align": "center" + }, + { + "id": "track-num", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "number" + } + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "track-art", + "component": "Image", + "url": { + "path": "art" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["track-title", "track-artist"] + }, + { + "id": "track-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "track-artist", + "component": "Text", + "text": { + "path": "artist" + }, + "variant": "caption" + }, + { + "id": "track-duration", + "component": "Text", + "text": { + "path": "duration" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-track-list", + "value": { + "playlistName": "Focus Flow", + "tracks": [ + { + "number": 1, + "art": "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=50&h=50&fit=crop", + "title": "Weightless", + "artist": "Marconi Union", + "duration": "8:09" + }, + { + "number": 2, + "art": "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=50&h=50&fit=crop", + "title": "Clair de Lune", + "artist": "Debussy", + "duration": "5:12" + }, + { + "number": 3, + "art": "https://images.unsplash.com/photo-1507838153414-b4b713384a76?w=50&h=50&fit=crop", + "title": "Ambient Light", + "artist": "Brian Eno", + "duration": "6:45" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/19_software-purchase.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/19_software-purchase.json new file mode 100644 index 0000000000..ee879eed36 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/19_software-purchase.json @@ -0,0 +1,194 @@ +{ + "name": "Software Purchase", + "description": "Example of software purchase demonstrating currency formatting and ChoicePicker.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-software-purchase", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-software-purchase", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "title", + "product-name", + "divider1", + "options", + "divider2", + "total-row", + "actions" + ] + }, + { + "id": "title", + "component": "Text", + "text": "Purchase License", + "variant": "h3" + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h2" + }, + { + "id": "divider1", + "component": "Divider" + }, + { + "id": "options", + "component": "Column", + "children": ["seats-row", "period-row"] + }, + { + "id": "seats-row", + "component": "Row", + "children": ["seats-label", "seats-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "seats-label", + "component": "Text", + "text": "Number of seats", + "variant": "body" + }, + { + "id": "seats-value", + "component": "Text", + "text": { + "path": "/seats" + }, + "variant": "h4" + }, + { + "id": "period-row", + "component": "Row", + "children": ["period-label", "period-picker"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "period-label", + "component": "Text", + "text": "Billing period", + "variant": "body" + }, + { + "id": "period-picker", + "component": "ChoicePicker", + "options": [ + { + "label": "Annual", + "value": "annual" + }, + { + "label": "Monthly", + "value": "monthly" + } + ], + "value": { + "path": "/billingPeriod" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "divider2", + "component": "Divider" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatCurrency(value: ${/total}, currency: 'USD')}/year" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "actions", + "component": "Row", + "children": ["confirm-btn", "cancel-btn"] + }, + { + "id": "confirm-btn-text", + "component": "Text", + "text": "Confirm Purchase" + }, + { + "id": "confirm-btn", + "component": "Button", + "child": "confirm-btn-text", + "action": { + "event": { + "name": "confirm", + "context": {} + } + } + }, + { + "id": "cancel-btn-text", + "component": "Text", + "text": "Cancel" + }, + { + "id": "cancel-btn", + "component": "Button", + "child": "cancel-btn-text", + "action": { + "event": { + "name": "cancel", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-software-purchase", + "value": { + "productName": "Design Suite Pro", + "seats": "10 seats", + "billingPeriod": ["annual"], + "total": 1188.0 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/20_restaurant-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/20_restaurant-card.json new file mode 100644 index 0000000000..ee02cbc2bc --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/20_restaurant-card.json @@ -0,0 +1,140 @@ +{ + "name": "Restaurant Card", + "description": "Example of restaurant card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-restaurant-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-restaurant-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["restaurant-image", "content"] + }, + { + "id": "restaurant-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["name-row", "cuisine", "rating-row", "details-row"] + }, + { + "id": "name-row", + "component": "Row", + "children": ["restaurant-name", "price-range"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "restaurant-name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "price-range", + "component": "Text", + "text": { + "path": "/priceRange" + }, + "variant": "body" + }, + { + "id": "cuisine", + "component": "Text", + "text": { + "path": "/cuisine" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "reviews"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "path": "/reviewCount" + }, + "variant": "caption" + }, + { + "id": "details-row", + "component": "Row", + "children": ["distance", "delivery-time"] + }, + { + "id": "distance", + "component": "Text", + "text": { + "path": "/distance" + }, + "variant": "caption" + }, + { + "id": "delivery-time", + "component": "Text", + "text": { + "path": "/deliveryTime" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-restaurant-card", + "value": { + "image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=300&h=150&fit=crop", + "name": "The Italian Kitchen", + "priceRange": "$$$", + "cuisine": "Italian \u2022 Pasta \u2022 Wine Bar", + "rating": "4.8", + "reviewCount": "(2,847 reviews)", + "distance": "0.8 mi", + "deliveryTime": "25-35 min" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/21_shipping-status.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/21_shipping-status.json new file mode 100644 index 0000000000..d9ea2317b4 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/21_shipping-status.json @@ -0,0 +1,137 @@ +{ + "name": "Shipping Status", + "description": "Example of shipping status demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-shipping-status", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-shipping-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "tracking-number", "divider", "steps-list", "eta"] + }, + { + "id": "header", + "component": "Row", + "children": ["package-icon", "title"], + "align": "center" + }, + { + "id": "package-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "title", + "component": "Text", + "text": "Package Status", + "variant": "h3" + }, + { + "id": "tracking-number", + "component": "Text", + "text": { + "path": "/trackingNumber" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "steps-list", + "component": "Column", + "children": { + "path": "/steps", + "componentId": "step-item-template" + } + }, + { + "id": "step-item-template", + "component": "Row", + "children": ["step-icon", "step-text"], + "align": "center" + }, + { + "id": "step-icon", + "component": "Icon", + "name": { + "path": "icon" + } + }, + { + "id": "step-text", + "component": "Text", + "text": { + "path": "label" + }, + "variant": "body" + }, + { + "id": "eta", + "component": "Row", + "children": ["eta-icon", "eta-text"], + "align": "center" + }, + { + "id": "eta-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "eta-text", + "component": "Text", + "text": { + "path": "/eta" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-shipping-status", + "value": { + "trackingNumber": "Tracking: 1Z999AA10123456784", + "steps": [ + { + "icon": "check", + "label": "Order Placed" + }, + { + "icon": "check", + "label": "Shipped" + }, + { + "icon": "send", + "label": "Out for Delivery" + }, + { + "icon": "check", + "label": "Delivered" + } + ], + "eta": "Estimated delivery: Today by 8 PM" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/22_credit-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/22_credit-card.json new file mode 100644 index 0000000000..5405814388 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/22_credit-card.json @@ -0,0 +1,117 @@ +{ + "name": "Credit Card", + "description": "Example of credit card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-credit-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-credit-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["card-type-row", "card-number", "card-details"] + }, + { + "id": "card-type-row", + "component": "Row", + "children": ["card-icon", "card-type"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "card-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "card-type", + "component": "Text", + "text": { + "path": "/cardType" + }, + "variant": "h4" + }, + { + "id": "card-number", + "component": "Text", + "text": { + "path": "/cardNumber" + }, + "variant": "h2" + }, + { + "id": "card-details", + "component": "Row", + "children": ["holder-col", "expiry-col"], + "justify": "spaceBetween" + }, + { + "id": "holder-col", + "component": "Column", + "children": ["holder-label", "holder-name"] + }, + { + "id": "holder-label", + "component": "Text", + "text": "CARD HOLDER", + "variant": "caption" + }, + { + "id": "holder-name", + "component": "Text", + "text": { + "path": "/holderName" + }, + "variant": "body" + }, + { + "id": "expiry-col", + "component": "Column", + "children": ["expiry-label", "expiry-date"], + "align": "end" + }, + { + "id": "expiry-label", + "component": "Text", + "text": "EXPIRES", + "variant": "caption" + }, + { + "id": "expiry-date", + "component": "Text", + "text": { + "path": "/expiryDate" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-credit-card", + "value": { + "cardType": "VISA", + "cardNumber": "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 4242", + "holderName": "SARAH JOHNSON", + "expiryDate": "09/27" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/23_step-counter.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/23_step-counter.json new file mode 100644 index 0000000000..66b2e3e670 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/23_step-counter.json @@ -0,0 +1,149 @@ +{ + "name": "Step Counter", + "description": "Example of step counter demonstrating number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-step-counter", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-step-counter", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "steps-display", "goal-text", "divider", "stats-row"], + "align": "center" + }, + { + "id": "header", + "component": "Row", + "children": ["steps-icon", "title"], + "align": "center" + }, + { + "id": "steps-icon", + "component": "Icon", + "name": "person" + }, + { + "id": "title", + "component": "Text", + "text": "Today's Steps", + "variant": "h4" + }, + { + "id": "steps-display", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/steps" + } + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "goal-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/progress}% of ${formatNumber(value: ${/goal})} goal" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["distance-col", "calories-col"], + "justify": "spaceAround" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} mi" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-step-counter", + "value": { + "steps": 8432, + "goal": 10000, + "progress": 84, + "distance": 3.8, + "calories": 312 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/24_recipe-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/24_recipe-card.json new file mode 100644 index 0000000000..a150f99ecb --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/24_recipe-card.json @@ -0,0 +1,204 @@ +{ + "name": "Recipe Card", + "description": "Example of recipe card demonstrating Tabs and pluralization.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-recipe-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-recipe-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "tabs-container" + }, + { + "id": "tabs-container", + "component": "Tabs", + "tabs": [ + { + "title": "Overview", + "child": "overview-col" + }, + { + "title": "Ingredients", + "child": "ingredients-list" + }, + { + "title": "Instructions", + "child": "instructions-list" + } + ] + }, + { + "id": "overview-col", + "component": "Column", + "children": ["recipe-image", "overview-content"] + }, + { + "id": "recipe-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "overview-content", + "component": "Column", + "children": ["title", "rating-row", "times-row", "servings"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "review-count"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "review-count", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "times-row", + "component": "Row", + "children": ["prep-time", "cook-time"] + }, + { + "id": "prep-time", + "component": "Row", + "children": ["prep-icon", "prep-text"], + "align": "center" + }, + { + "id": "prep-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "prep-text", + "component": "Text", + "text": { + "path": "/prepTime" + }, + "variant": "caption" + }, + { + "id": "cook-time", + "component": "Row", + "children": ["cook-icon", "cook-text"], + "align": "center" + }, + { + "id": "cook-icon", + "component": "Icon", + "name": "warning" + }, + { + "id": "cook-text", + "component": "Text", + "text": { + "path": "/cookTime" + }, + "variant": "caption" + }, + { + "id": "servings", + "component": "Text", + "text": { + "path": "/servings" + }, + "variant": "caption" + }, + { + "id": "ingredients-list", + "component": "Column", + "children": { + "path": "/ingredients", + "componentId": "item-template" + } + }, + { + "id": "instructions-list", + "component": "Column", + "children": { + "path": "/instructions", + "componentId": "item-template" + } + }, + { + "id": "item-template", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-recipe-card", + "value": { + "image": "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=180&fit=crop", + "title": "Mediterranean Quinoa Bowl", + "rating": "4.9", + "reviewCount": 1247, + "prepTime": "15 min prep", + "cookTime": "20 min cook", + "servings": "Serves 4", + "ingredients": [ + {"text": "1 cup quinoa"}, + {"text": "2 cups water"}, + {"text": "1 cucumber, diced"}, + {"text": "1 cup cherry tomatoes, halved"} + ], + "instructions": [ + {"text": "1. Rinse quinoa and bring to a boil in water."}, + {"text": "2. Reduce heat and simmer for 15 minutes."}, + {"text": "3. Fluff with a fork and let cool."}, + {"text": "4. Mix with diced vegetables."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/25_contact-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/25_contact-card.json new file mode 100644 index 0000000000..4dd7155c1a --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/25_contact-card.json @@ -0,0 +1,175 @@ +{ + "name": "Contact Card", + "description": "Example of contact card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-contact-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["avatar-image", "name", "title", "divider", "contact-info", "actions"], + "align": "center" + }, + { + "id": "avatar-image", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "contact-info", + "component": "Column", + "children": ["phone-row", "email-row", "location-row"] + }, + { + "id": "phone-row", + "component": "Row", + "children": ["phone-icon", "phone-text"], + "align": "center" + }, + { + "id": "phone-icon", + "component": "Icon", + "name": "phone" + }, + { + "id": "phone-text", + "component": "Text", + "text": { + "path": "/phone" + }, + "variant": "body" + }, + { + "id": "email-row", + "component": "Row", + "children": ["email-icon", "email-text"], + "align": "center" + }, + { + "id": "email-icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "email-text", + "component": "Text", + "text": { + "path": "/email" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["call-btn", "message-btn"] + }, + { + "id": "call-btn-text", + "component": "Text", + "text": "Call" + }, + { + "id": "call-btn", + "component": "Button", + "child": "call-btn-text", + "action": { + "event": { + "name": "call", + "context": {} + } + } + }, + { + "id": "message-btn-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-btn", + "component": "Button", + "child": "message-btn-text", + "action": { + "event": { + "name": "message", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-contact-card", + "value": { + "avatar": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop", + "name": "David Park", + "title": "Engineering Manager", + "phone": "+1 (555) 234-5678", + "email": "david.park@company.com", + "location": "San Francisco, CA" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/26_podcast-episode.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/26_podcast-episode.json new file mode 100644 index 0000000000..0e3b2b806e --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/26_podcast-episode.json @@ -0,0 +1,123 @@ +{ + "name": "Podcast Episode", + "description": "Example of podcast episode demonstrating AudioPlayer and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-podcast-episode", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-podcast-episode", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["artwork", "content"], + "align": "start" + }, + { + "id": "artwork", + "component": "Image", + "url": { + "path": "/artwork" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["show-name", "episode-title", "meta-row", "description", "audio-player"] + }, + { + "id": "show-name", + "component": "Text", + "text": { + "path": "/showName" + }, + "variant": "caption" + }, + { + "id": "episode-title", + "component": "Text", + "text": { + "path": "/episodeTitle" + }, + "variant": "h4" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["duration", "date"] + }, + { + "id": "duration", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "MMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "audio-player", + "component": "AudioPlayer", + "url": { + "path": "/audioUrl" + }, + "description": { + "path": "/episodeTitle" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-podcast-episode", + "value": { + "artwork": "https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=100&h=100&fit=crop", + "showName": "Tech Talk Daily", + "episodeTitle": "The Future of AI in Product Design", + "duration": "45 min", + "date": "2024-12-15", + "description": "How AI is transforming the way we design and build products.", + "audioUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/27_stats-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/27_stats-card.json new file mode 100644 index 0000000000..8a477e6383 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/27_stats-card.json @@ -0,0 +1,106 @@ +{ + "name": "Stats Card", + "description": "Example of stats card demonstrating currency and number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-stats-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-stats-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "value", "trend-row"] + }, + { + "id": "header", + "component": "Row", + "children": ["metric-icon", "metric-name"], + "align": "center" + }, + { + "id": "metric-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "metric-name", + "component": "Text", + "text": { + "path": "/metricName" + }, + "variant": "caption" + }, + { + "id": "value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/value" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "trend-row", + "component": "Row", + "children": ["trend-icon", "trend-text"], + "align": "center" + }, + { + "id": "trend-icon", + "component": "Icon", + "name": { + "path": "/trendIcon" + } + }, + { + "id": "trend-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "+${/trendPercent}% from last month" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-stats-card", + "value": { + "icon": "trending_up", + "metricName": "Monthly Revenue", + "value": 48294, + "trendIcon": "arrow_upward", + "trendPercent": 12.5 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/28_countdown-timer.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/28_countdown-timer.json new file mode 100644 index 0000000000..8513e17f36 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/28_countdown-timer.json @@ -0,0 +1,135 @@ +{ + "name": "Countdown Timer", + "description": "Example of countdown timer demonstrating date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-countdown-timer", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-countdown-timer", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["event-name", "countdown-row", "target-date"], + "align": "center" + }, + { + "id": "event-name", + "component": "Text", + "text": { + "path": "/eventName" + }, + "variant": "h3" + }, + { + "id": "countdown-row", + "component": "Row", + "children": ["days-col", "hours-col", "minutes-col"], + "justify": "spaceAround" + }, + { + "id": "days-col", + "component": "Column", + "children": ["days-value", "days-label"], + "align": "center" + }, + { + "id": "days-value", + "component": "Text", + "text": { + "path": "/days" + }, + "variant": "h1" + }, + { + "id": "days-label", + "component": "Text", + "text": "Days", + "variant": "caption" + }, + { + "id": "hours-col", + "component": "Column", + "children": ["hours-value", "hours-label"], + "align": "center" + }, + { + "id": "hours-value", + "component": "Text", + "text": { + "path": "/hours" + }, + "variant": "h1" + }, + { + "id": "hours-label", + "component": "Text", + "text": "Hours", + "variant": "caption" + }, + { + "id": "minutes-col", + "component": "Column", + "children": ["minutes-value", "minutes-label"], + "align": "center" + }, + { + "id": "minutes-value", + "component": "Text", + "text": { + "path": "/minutes" + }, + "variant": "h1" + }, + { + "id": "minutes-label", + "component": "Text", + "text": "Minutes", + "variant": "caption" + }, + { + "id": "target-date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/targetDate" + }, + "format": "MMMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-countdown-timer", + "value": { + "eventName": "Product Launch", + "days": "14", + "hours": "08", + "minutes": "32", + "targetDate": "2025-01-15" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/29_movie-card.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/29_movie-card.json new file mode 100644 index 0000000000..ab5a25d182 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/29_movie-card.json @@ -0,0 +1,156 @@ +{ + "name": "Movie Card", + "description": "Example of movie card demonstrating Modal and Video components.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-movie-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-movie-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["poster", "content", "trailer-modal"] + }, + { + "id": "poster", + "component": "Image", + "url": { + "path": "/poster" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["title-row", "genre", "rating-row", "runtime", "watch-trailer-btn"] + }, + { + "id": "title-row", + "component": "Row", + "children": ["movie-title", "year"], + "align": "start" + }, + { + "id": "movie-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "year", + "component": "Text", + "text": { + "path": "/year" + }, + "variant": "caption" + }, + { + "id": "genre", + "component": "Text", + "text": { + "path": "/genre" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating-value"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating-value", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "runtime", + "component": "Row", + "children": ["time-icon", "runtime-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "runtime-text", + "component": "Text", + "text": { + "path": "/runtime" + }, + "variant": "caption" + }, + { + "id": "watch-trailer-btn-text", + "component": "Text", + "text": "Watch Trailer" + }, + { + "id": "watch-trailer-btn", + "component": "Button", + "child": "watch-trailer-btn-text", + "action": { + "event": { + "name": "open_trailer" + } + } + }, + { + "id": "trailer-modal", + "component": "Modal", + "trigger": "watch-trailer-btn", + "content": "trailer-video" + }, + { + "id": "trailer-video", + "component": "Video", + "url": { + "path": "/trailerUrl" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-movie-card", + "value": { + "poster": "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=200&h=300&fit=crop", + "title": "Interstellar", + "year": "(2014)", + "genre": "Sci-Fi • Adventure • Drama", + "rating": "8.7/10", + "runtime": "2h 49min", + "trailerUrl": "https://www.w3schools.com/html/mov_bbb.mp4" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/30_live-invitation-builder.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/30_live-invitation-builder.json new file mode 100644 index 0000000000..06fdd741d3 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/30_live-invitation-builder.json @@ -0,0 +1,205 @@ +{ + "name": "Live Invitation Builder", + "description": "Demonstrates reactive two-way binding where editor inputs update a live preview in real-time.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-invitation-builder", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-invitation-builder", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "main-content"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "# Invitation Builder", + "variant": "h1" + }, + { + "id": "main-content", + "component": "Row", + "children": ["editor-col", "preview-col"], + "align": "start" + }, + { + "id": "editor-col", + "component": "Column", + "children": [ + "editor-title", + "event-name-input", + "guest-input", + "date-input", + "location-picker" + ], + "weight": 1, + "align": "stretch" + }, + { + "id": "editor-title", + "component": "Text", + "text": "Customize your invitation", + "variant": "h3" + }, + { + "id": "event-name-input", + "component": "TextField", + "label": "Event Name", + "value": { + "path": "/event/name" + } + }, + { + "id": "guest-input", + "component": "TextField", + "label": "Guest of Honor", + "value": { + "path": "/event/guest" + } + }, + { + "id": "date-input", + "component": "DateTimeInput", + "label": "Event Date & Time", + "value": { + "path": "/event/date" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "location-picker", + "component": "ChoicePicker", + "label": "Location", + "options": [ + {"label": "Grand Ballroom", "value": "ballroom"}, + {"label": "Sunset Terrace", "value": "terrace"}, + {"label": "Garden Pavillion", "value": "garden"} + ], + "value": { + "path": "/event/location" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "preview-col", + "component": "Column", + "children": ["preview-title", "invitation-card"], + "weight": 1, + "align": "center" + }, + { + "id": "preview-title", + "component": "Text", + "text": "Live Preview", + "variant": "caption" + }, + { + "id": "invitation-card", + "component": "Card", + "child": "invitation-content" + }, + { + "id": "invitation-content", + "component": "Column", + "children": [ + "invite-image", + "invite-event-name", + "invite-guest-row", + "invite-date-text", + "invite-location-text" + ], + "align": "center" + }, + { + "id": "invite-image", + "component": "Image", + "url": "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop", + "variant": "mediumFeature" + }, + { + "id": "invite-event-name", + "component": "Text", + "text": { + "path": "/event/name" + }, + "variant": "h2" + }, + { + "id": "invite-guest-row", + "component": "Row", + "children": ["invite-for-text", "invite-guest-name"], + "align": "center" + }, + { + "id": "invite-for-text", + "component": "Text", + "text": "Celebrating", + "variant": "body" + }, + { + "id": "invite-guest-name", + "component": "Text", + "text": { + "path": "/event/guest" + }, + "variant": "h3" + }, + { + "id": "invite-date-text", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/event/date" + }, + "format": "EEEE, MMMM d, yyyy 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "invite-location-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Location: ${/event/location/0}" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-invitation-builder", + "value": { + "event": { + "name": "Summer Gala", + "guest": "Alex Johnson", + "date": "2025-07-15T19:00:00Z", + "location": ["terrace"] + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/31_incremental-dashboard.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/31_incremental-dashboard.json new file mode 100644 index 0000000000..775a2de37e --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/31_incremental-dashboard.json @@ -0,0 +1,128 @@ +{ + "name": "Incremental Dashboard", + "description": "Demonstrates structural evolution of a UI where loading placeholders are incrementally replaced by actual components.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-incremental-dashboard", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "content-grid"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "System Dashboard", + "variant": "h2" + }, + { + "id": "content-grid", + "component": "Row", + "children": ["left-panel", "right-panel"] + }, + { + "id": "left-panel", + "component": "Column", + "children": ["panel-a-loading"], + "weight": 1 + }, + { + "id": "right-panel", + "component": "Column", + "children": ["panel-b-loading"], + "weight": 1 + }, + { + "id": "panel-a-loading", + "component": "Text", + "text": "Loading analytics...", + "variant": "caption" + }, + { + "id": "panel-b-loading", + "component": "Text", + "text": "Loading logs...", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "left-panel", + "component": "Column", + "children": ["analytics-card"], + "weight": 1 + }, + { + "id": "analytics-card", + "component": "Card", + "child": "analytics-text" + }, + { + "id": "analytics-text", + "component": "Text", + "text": "Analytics are ready.", + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "right-panel", + "component": "Column", + "children": ["logs-list"], + "weight": 1 + }, + { + "id": "logs-list", + "component": "List", + "children": { + "path": "/logs", + "componentId": "log-template" + } + }, + { + "id": "log-template", + "component": "Text", + "text": {"path": "message"}, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-incremental-dashboard", + "value": { + "logs": [ + {"message": "System boot complete."}, + {"message": "All services healthy."}, + {"message": "Waiting for user input."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/32_advanced-form-validator.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/32_advanced-form-validator.json new file mode 100644 index 0000000000..0279bf34be --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/32_advanced-form-validator.json @@ -0,0 +1,166 @@ +{ + "name": "Advanced Form Validator", + "description": "Demonstrates complex validation logic and deeply nested formatting functions.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-advanced-validator", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-advanced-validator", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "welcome-text", + "email-field", + "phone-field", + "zip-field", + "terms-checkbox", + "submit-btn" + ], + "align": "stretch" + }, + { + "id": "welcome-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Hello! Today is ${formatDate(value: ${/now}, format: 'EEEE, MMMM d')}." + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "email-field", + "component": "TextField", + "label": "Email Address", + "value": {"path": "/formData/email"}, + "checks": [ + { + "condition": { + "call": "email", + "args": {"value": {"path": "/formData/email"}} + }, + "message": "Invalid email format" + } + ] + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone Number", + "value": {"path": "/formData/phone"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": { + "value": {"path": "/formData/phone"}, + "pattern": "^\\+?[0-9]{10,15}$" + } + }, + "message": "Invalid phone format" + } + ] + }, + { + "id": "zip-field", + "component": "TextField", + "label": "Zip Code", + "value": {"path": "/formData/zip"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": {"value": {"path": "/formData/zip"}, "pattern": "^[0-9]{5}$"} + }, + "message": "Must be exactly 5 digits" + } + ] + }, + { + "id": "terms-checkbox", + "component": "CheckBox", + "label": "I agree to the terms and conditions", + "value": {"path": "/formData/agree"} + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit Registration" + }, + { + "id": "submit-btn", + "component": "Button", + "child": "submit-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + {"path": "/formData/agree"}, + { + "call": "or", + "args": { + "values": [ + { + "call": "required", + "args": {"value": {"path": "/formData/email"}} + }, + { + "call": "required", + "args": {"value": {"path": "/formData/phone"}} + } + ] + } + }, + {"call": "required", "args": {"value": {"path": "/formData/zip"}}} + ] + } + }, + "message": "You must agree to terms AND provide either Email or Phone, plus a Zip code." + } + ], + "action": { + "event": { + "name": "register", + "context": {"data": {"path": "/formData"}} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-advanced-validator", + "value": { + "now": "2025-12-15T12:00:00Z", + "formData": { + "email": "", + "phone": "", + "zip": "", + "agree": false + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/33_financial-data-grid.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/33_financial-data-grid.json new file mode 100644 index 0000000000..7d19eb8411 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/33_financial-data-grid.json @@ -0,0 +1,171 @@ +{ + "name": "Financial Data Grid", + "description": "Demonstrates complex layout weighting and alignment using Rows and Columns with templates.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-financial-grid", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-financial-grid", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "grid-list"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["col-asset", "col-price", "col-change", "col-market-cap"], + "align": "center" + }, + { + "id": "col-asset", + "component": "Text", + "text": "Asset", + "variant": "caption", + "weight": 2 + }, + { + "id": "col-price", + "component": "Text", + "text": "Price", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-change", + "component": "Text", + "text": "24h Change", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-market-cap", + "component": "Text", + "text": "Market Cap", + "variant": "caption", + "weight": 1.5 + }, + { + "id": "grid-list", + "component": "List", + "children": { + "path": "/assets", + "componentId": "row-template" + } + }, + { + "id": "row-template", + "component": "Row", + "children": ["asset-info", "asset-price", "asset-change", "asset-market-cap"], + "align": "center" + }, + { + "id": "asset-info", + "component": "Row", + "children": ["asset-icon", "asset-name-col"], + "weight": 2, + "align": "center" + }, + { + "id": "asset-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "asset-name-col", + "component": "Column", + "children": ["asset-name", "asset-symbol"] + }, + { + "id": "asset-name", + "component": "Text", + "text": {"path": "name"}, + "variant": "body" + }, + { + "id": "asset-symbol", + "component": "Text", + "text": {"path": "symbol"}, + "variant": "caption" + }, + { + "id": "asset-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "price"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-change", + "component": "Text", + "text": { + "call": "formatString", + "args": {"value": "${change}%"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-market-cap", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "marketCap"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1.5 + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-financial-grid", + "value": { + "assets": [ + { + "name": "Bitcoin", + "symbol": "BTC", + "price": 43500.25, + "change": 1.2, + "marketCap": 850000000000 + }, + { + "name": "Ethereum", + "symbol": "ETH", + "price": 2250.5, + "change": -0.5, + "marketCap": 270000000000 + }, + { + "name": "Solana", + "symbol": "SOL", + "price": 95.8, + "change": 5.4, + "marketCap": 40000000000 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/34_child-list-template.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/34_child-list-template.json new file mode 100644 index 0000000000..328420b9f7 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/34_child-list-template.json @@ -0,0 +1,80 @@ +{ + "name": "ChildList Template Expansion", + "description": "Demonstrates dynamic list generation using ChildList object templates.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-child-list-template", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-child-list-template", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "item-list"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Dynamic Item List", + "variant": "h3" + }, + { + "id": "item-list", + "component": "List", + "children": { + "componentId": "item-row", + "path": "/items" + } + }, + { + "id": "item-row", + "component": "Row", + "children": ["item-name", "qty-label", "item-qty"] + }, + { + "id": "item-name", + "component": "Text", + "text": {"path": "name"} + }, + { + "id": "qty-label", + "component": "Text", + "text": " - Qty: " + }, + { + "id": "item-qty", + "component": "Text", + "text": {"path": "quantity"} + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-child-list-template", + "value": { + "items": [ + {"name": "Apple", "quantity": 10}, + {"name": "Banana", "quantity": 5}, + {"name": "Cherry", "quantity": 20} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/35_markdown-text.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/35_markdown-text.json new file mode 100644 index 0000000000..a1f0e5fca9 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/35_markdown-text.json @@ -0,0 +1,44 @@ +{ + "name": "Markdown Text Support", + "description": "Demonstrates Markdown rendering in Text component.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-markdown-text", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-markdown-text", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "markdown-content"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Markdown Rendering", + "variant": "h3" + }, + { + "id": "markdown-content", + "component": "Text", + "text": "# Heading 1\n\nThis is **bold** text and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link to Google](https://google.com)" + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/36_modal.json b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/36_modal.json new file mode 100644 index 0000000000..f17e3068e8 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/testdata/v0_9/catalogs/basic/examples/36_modal.json @@ -0,0 +1,65 @@ +{ + "name": "Modal Sample", + "description": "Example of Modal component showing a trigger and content.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "modal-sample-surface", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "modal-sample-surface", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["title", "modal-comp"] + }, + { + "id": "title", + "component": "Text", + "text": "Modal Component Sample", + "variant": "h2" + }, + { + "id": "modal-comp", + "component": "Modal", + "trigger": "open-btn", + "content": "modal-content" + }, + { + "id": "open-btn-text", + "component": "Text", + "text": "Open Modal" + }, + { + "id": "open-btn", + "component": "Button", + "child": "open-btn-text", + "action": { + "event": { + "name": "openModalEvent", + "context": {} + } + } + }, + { + "id": "modal-content", + "component": "Column", + "children": ["modal-text"] + }, + { + "id": "modal-text", + "component": "Text", + "text": "This is the content inside the modal." + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v09/zz_component.go b/agent_sdks/go/a2ui/v09/zz_component.go new file mode 100644 index 0000000000..31a036f380 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_component.go @@ -0,0 +1,136 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +// TabDef defines a tab within a [Tabs] component. +type TabDef struct { + Title DynamicString `json:"title"` + Child string `json:"child"` +} + +// ChoiceOption defines a selectable option within a [ChoicePicker] component. +type ChoiceOption struct { + Label DynamicString `json:"label"` + Value string `json:"value"` +} + +// AudioPlayerComponent holds the component-specific fields for a AudioPlayer. +type AudioPlayerComponent struct { + Description *DynamicString `json:"description,omitempty"` + URL DynamicString `json:"url"` +} + +// ButtonComponent holds the component-specific fields for a Button. +type ButtonComponent struct { + Action Action `json:"action"` + Child string `json:"child"` + Variant ButtonVariant `json:"variant,omitempty"` +} + +// CardComponent holds the component-specific fields for a Card. +type CardComponent struct { + Child string `json:"child"` +} + +// CheckBoxComponent holds the component-specific fields for a CheckBox. +type CheckBoxComponent struct { + Label DynamicString `json:"label"` + Value DynamicBoolean `json:"value"` +} + +// ChoicePickerComponent holds the component-specific fields for a ChoicePicker. +type ChoicePickerComponent struct { + DisplayStyle ChoicePickerDisplayStyle `json:"displayStyle,omitempty"` + Filterable *bool `json:"filterable,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Options []ChoiceOption `json:"options"` + Value DynamicStringList `json:"value"` + Variant ChoicePickerVariant `json:"variant,omitempty"` +} + +// ColumnComponent holds the component-specific fields for a Column. +type ColumnComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// DateTimeInputComponent holds the component-specific fields for a DateTimeInput. +type DateTimeInputComponent struct { + EnableDate *bool `json:"enableDate,omitempty"` + EnableTime *bool `json:"enableTime,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Max *DynamicString `json:"max,omitempty"` + Min *DynamicString `json:"min,omitempty"` + Value DynamicString `json:"value"` +} + +// DividerComponent holds the component-specific fields for a Divider. +type DividerComponent struct { + Axis DividerAxis `json:"axis,omitempty"` +} + +// IconComponent holds the component-specific fields for a Icon. +type IconComponent struct { + Name IconNameOrPath `json:"name"` +} + +// ImageComponent holds the component-specific fields for a Image. +type ImageComponent struct { + Description *DynamicString `json:"description,omitempty"` + Fit ImageFit `json:"fit,omitempty"` + URL DynamicString `json:"url"` + Variant ImageVariant `json:"variant,omitempty"` +} + +// ListComponent holds the component-specific fields for a List. +type ListComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Direction ListDirection `json:"direction,omitempty"` +} + +// ModalComponent holds the component-specific fields for a Modal. +type ModalComponent struct { + Content string `json:"content"` + Trigger string `json:"trigger"` +} + +// RowComponent holds the component-specific fields for a Row. +type RowComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// SliderComponent holds the component-specific fields for a Slider. +type SliderComponent struct { + Label *DynamicString `json:"label,omitempty"` + Max float64 `json:"max"` + Min *float64 `json:"min,omitempty"` + Value DynamicNumber `json:"value"` +} + +// TabsComponent holds the component-specific fields for a Tabs. +type TabsComponent struct { + Tabs []TabDef `json:"tabs"` +} + +// TextComponent holds the component-specific fields for a Text. +type TextComponent struct { + Text DynamicString `json:"text"` + Variant TextVariant `json:"variant,omitempty"` +} + +// TextFieldComponent holds the component-specific fields for a TextField. +type TextFieldComponent struct { + Label DynamicString `json:"label"` + ValidationRegexp string `json:"validationRegexp,omitempty"` + Value *DynamicString `json:"value,omitempty"` + Variant TextFieldVariant `json:"variant,omitempty"` +} + +// VideoComponent holds the component-specific fields for a Video. +type VideoComponent struct { + URL DynamicString `json:"url"` +} diff --git a/agent_sdks/go/a2ui/v09/zz_component_marshal.go b/agent_sdks/go/a2ui/v09/zz_component_marshal.go new file mode 100644 index 0000000000..ccf19e9e96 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_component_marshal.go @@ -0,0 +1,200 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON encodes a [Component] as a flat JSON object with the "component" +// discriminator, common fields, and component-specific fields merged together. +func (c Component) MarshalJSON() ([]byte, error) { + type common struct { + ComponentType string `json:"component"` + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + componentType, specific, count := c.componentData() + switch count { + case 0: + return nil, fmt.Errorf("a2ui: component has no concrete type set") + case 1: + default: + return nil, fmt.Errorf("a2ui: component has multiple concrete types set") + } + cm := common{ + ComponentType: componentType, + ID: c.ID, + Accessibility: c.Accessibility, + Weight: c.Weight, + Checks: c.Checks, + } + + commonBytes, err := json.Marshal(cm) + if err != nil { + return nil, err + } + + specificBytes, err := json.Marshal(specific) + if err != nil { + return nil, err + } + + // Merge: overwrite common with specific fields. + if len(specificBytes) <= 2 { // "{}" or empty + return commonBytes, nil + } + // Remove leading '{' from specific, append to common. + merged := make([]byte, 0, len(commonBytes)+len(specificBytes)) + merged = append(merged, commonBytes[:len(commonBytes)-1]...) // drop trailing '}' + merged = append(merged, ',') + merged = append(merged, specificBytes[1:]...) // drop leading '{' + return merged, nil +} + +// UnmarshalJSON decodes a flat JSON object into a [Component], using the +// "component" field as the discriminator. +func (c *Component) UnmarshalJSON(data []byte) error { + // Read the discriminator. + var disc struct { + ComponentType string `json:"component"` + } + if err := json.Unmarshal(data, &disc); err != nil { + return err + } + + // Unmarshal common fields. + type commonOnly struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + var cm commonOnly + if err := json.Unmarshal(data, &cm); err != nil { + return err + } + *c = Component{} + c.ID = cm.ID + c.Accessibility = cm.Accessibility + c.Weight = cm.Weight + c.Checks = cm.Checks + + // Unmarshal component-specific fields. + switch disc.ComponentType { + case "AudioPlayer": + var v AudioPlayerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.AudioPlayer = &v + case "Button": + var v ButtonComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Button = &v + case "Card": + var v CardComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Card = &v + case "CheckBox": + var v CheckBoxComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.CheckBox = &v + case "ChoicePicker": + var v ChoicePickerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.ChoicePicker = &v + case "Column": + var v ColumnComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Column = &v + case "DateTimeInput": + var v DateTimeInputComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.DateTimeInput = &v + case "Divider": + var v DividerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Divider = &v + case "Icon": + var v IconComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Icon = &v + case "Image": + var v ImageComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Image = &v + case "List": + var v ListComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.List = &v + case "Modal": + var v ModalComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Modal = &v + case "Row": + var v RowComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Row = &v + case "Slider": + var v SliderComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Slider = &v + case "Tabs": + var v TabsComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Tabs = &v + case "Text": + var v TextComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Text = &v + case "TextField": + var v TextFieldComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.TextField = &v + case "Video": + var v VideoComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Video = &v + default: + return fmt.Errorf("unknown component type: %s", disc.ComponentType) + } + return nil +} diff --git a/agent_sdks/go/a2ui/v09/zz_enum.go b/agent_sdks/go/a2ui/v09/zz_enum.go new file mode 100644 index 0000000000..2017ad9a4d --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_enum.go @@ -0,0 +1,129 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +// ReturnType is the expected return type of a function call. +type ReturnType string + +const ( + ReturnTypeString ReturnType = "string" + ReturnTypeNumber ReturnType = "number" + ReturnTypeBoolean ReturnType = "boolean" + ReturnTypeArray ReturnType = "array" + ReturnTypeObject ReturnType = "object" + ReturnTypeAny ReturnType = "any" + ReturnTypeVoid ReturnType = "void" +) + +// IconName identifies a built-in icon. +type IconName string + +// ButtonVariant defines the allowed values for the ButtonVariant enum. +type ButtonVariant string + +const ( + ButtonVariantDefault ButtonVariant = "default" + ButtonVariantPrimary ButtonVariant = "primary" + ButtonVariantBorderless ButtonVariant = "borderless" +) + +// ChoicePickerDisplayStyle defines the allowed values for the ChoicePickerDisplayStyle enum. +type ChoicePickerDisplayStyle string + +const ( + ChoicePickerDisplayStyleCheckbox ChoicePickerDisplayStyle = "checkbox" + ChoicePickerDisplayStyleChips ChoicePickerDisplayStyle = "chips" +) + +// ChoicePickerVariant defines the allowed values for the ChoicePickerVariant enum. +type ChoicePickerVariant string + +const ( + ChoicePickerVariantMultipleSelection ChoicePickerVariant = "multipleSelection" + ChoicePickerVariantMutuallyExclusive ChoicePickerVariant = "mutuallyExclusive" +) + +// DividerAxis defines the allowed values for the DividerAxis enum. +type DividerAxis string + +const ( + DividerAxisHorizontal DividerAxis = "horizontal" + DividerAxisVertical DividerAxis = "vertical" +) + +// ImageFit defines the allowed values for the ImageFit enum. +type ImageFit string + +const ( + ImageFitContain ImageFit = "contain" + ImageFitCover ImageFit = "cover" + ImageFitFill ImageFit = "fill" + ImageFitNone ImageFit = "none" + ImageFitScaleDown ImageFit = "scaleDown" +) + +// ImageVariant defines the allowed values for the ImageVariant enum. +type ImageVariant string + +const ( + ImageVariantIcon ImageVariant = "icon" + ImageVariantAvatar ImageVariant = "avatar" + ImageVariantSmallFeature ImageVariant = "smallFeature" + ImageVariantMediumFeature ImageVariant = "mediumFeature" + ImageVariantLargeFeature ImageVariant = "largeFeature" + ImageVariantHeader ImageVariant = "header" +) + +// LayoutAlign defines the allowed values for the LayoutAlign enum. +type LayoutAlign string + +const ( + LayoutAlignCenter LayoutAlign = "center" + LayoutAlignEnd LayoutAlign = "end" + LayoutAlignStart LayoutAlign = "start" + LayoutAlignStretch LayoutAlign = "stretch" +) + +// LayoutJustify defines the allowed values for the LayoutJustify enum. +type LayoutJustify string + +const ( + LayoutJustifyStart LayoutJustify = "start" + LayoutJustifyCenter LayoutJustify = "center" + LayoutJustifyEnd LayoutJustify = "end" + LayoutJustifySpaceBetween LayoutJustify = "spaceBetween" + LayoutJustifySpaceAround LayoutJustify = "spaceAround" + LayoutJustifySpaceEvenly LayoutJustify = "spaceEvenly" + LayoutJustifyStretch LayoutJustify = "stretch" +) + +// ListDirection defines the allowed values for the ListDirection enum. +type ListDirection string + +const ( + ListDirectionVertical ListDirection = "vertical" + ListDirectionHorizontal ListDirection = "horizontal" +) + +// TextFieldVariant defines the allowed values for the TextFieldVariant enum. +type TextFieldVariant string + +const ( + TextFieldVariantLongText TextFieldVariant = "longText" + TextFieldVariantNumber TextFieldVariant = "number" + TextFieldVariantShortText TextFieldVariant = "shortText" + TextFieldVariantObscured TextFieldVariant = "obscured" +) + +// TextVariant defines the allowed values for the TextVariant enum. +type TextVariant string + +const ( + TextVariantH1 TextVariant = "h1" + TextVariantH2 TextVariant = "h2" + TextVariantH3 TextVariant = "h3" + TextVariantH4 TextVariant = "h4" + TextVariantH5 TextVariant = "h5" + TextVariantCaption TextVariant = "caption" + TextVariantBody TextVariant = "body" +) diff --git a/agent_sdks/go/a2ui/v09/zz_function.go b/agent_sdks/go/a2ui/v09/zz_function.go new file mode 100644 index 0000000000..e59313f2f6 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_function.go @@ -0,0 +1,188 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +// And creates a function call for "and". +// Performs a logical AND operation on a list of boolean values. +func And(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "and", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Email creates a function call for "email". +// Checks that the value is a valid email address. +func Email(value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "email", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// FormatCurrency creates a function call for "formatCurrency". +// Formats a number as a currency string. +func FormatCurrency(currency DynamicString, decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatCurrency", + Args: map[string]any{ + "currency": currency, + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatDate creates a function call for "formatDate". +// Formats a timestamp into a string using a pattern. +func FormatDate(format DynamicString, value DynamicValue) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatDate", + Args: map[string]any{ + "format": format, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatNumber creates a function call for "formatNumber". +// Formats a number with the specified grouping and decimal precision. +func FormatNumber(decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatNumber", + Args: map[string]any{ + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatString creates a function call for "formatString". +// Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\${`. +func FormatString(value DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatString", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// Length creates a function call for "length". +// Checks string length constraints. +func Length(max int, min int, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "length", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Not creates a function call for "not". +// Performs a logical NOT operation on a boolean value. +func Not(value DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "not", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Numeric creates a function call for "numeric". +// Checks numeric range constraints. +func Numeric(max float64, min float64, value DynamicNumber) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "numeric", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// OpenURL creates a function call for "openUrl". +// Opens the specified URL in a browser or handler. This function has no return value. +func OpenURL(url string) Action { + return Action{FunctionCall: &FunctionCall{ + Call: "openUrl", + Args: map[string]any{ + "url": url, + }, + ReturnType: ReturnTypeVoid, + }} +} + +// Or creates a function call for "or". +// Performs a logical OR operation on a list of boolean values. +func Or(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "or", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Pluralize creates a function call for "pluralize". +// Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'. +func Pluralize(few DynamicString, many DynamicString, one DynamicString, other DynamicString, two DynamicString, value DynamicNumber, zero DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "pluralize", + Args: map[string]any{ + "few": few, + "many": many, + "one": one, + "other": other, + "two": two, + "value": value, + "zero": zero, + }, + ReturnType: ReturnTypeString, + }} +} + +// Regex creates a function call for "regex". +// Checks that the value matches a regular expression string. +func Regex(pattern string, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "regex", + Args: map[string]any{ + "pattern": pattern, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Required creates a function call for "required". +// Checks that the value is not null, undefined, or empty. +func Required(value DynamicValue) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "required", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} diff --git a/agent_sdks/go/a2ui/v09/zz_icon.go b/agent_sdks/go/a2ui/v09/zz_icon.go new file mode 100644 index 0000000000..8cb0332584 --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_icon.go @@ -0,0 +1,66 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +// Well-known icon names. +const ( + IconAccountCircle IconName = "accountCircle" + IconAdd IconName = "add" + IconArrowBack IconName = "arrowBack" + IconArrowForward IconName = "arrowForward" + IconAttachFile IconName = "attachFile" + IconCalendarToday IconName = "calendarToday" + IconCall IconName = "call" + IconCamera IconName = "camera" + IconCheck IconName = "check" + IconClose IconName = "close" + IconDelete IconName = "delete" + IconDownload IconName = "download" + IconEdit IconName = "edit" + IconEvent IconName = "event" + IconError IconName = "error" + IconFastForward IconName = "fastForward" + IconFavorite IconName = "favorite" + IconFavoriteOff IconName = "favoriteOff" + IconFolder IconName = "folder" + IconHelp IconName = "help" + IconHome IconName = "home" + IconInfo IconName = "info" + IconLocationOn IconName = "locationOn" + IconLock IconName = "lock" + IconLockOpen IconName = "lockOpen" + IconMail IconName = "mail" + IconMenu IconName = "menu" + IconMoreVert IconName = "moreVert" + IconMoreHoriz IconName = "moreHoriz" + IconNotificationsOff IconName = "notificationsOff" + IconNotifications IconName = "notifications" + IconPause IconName = "pause" + IconPayment IconName = "payment" + IconPerson IconName = "person" + IconPhone IconName = "phone" + IconPhoto IconName = "photo" + IconPlay IconName = "play" + IconPrint IconName = "print" + IconRefresh IconName = "refresh" + IconRewind IconName = "rewind" + IconSearch IconName = "search" + IconSend IconName = "send" + IconSettings IconName = "settings" + IconShare IconName = "share" + IconShoppingCart IconName = "shoppingCart" + IconSkipNext IconName = "skipNext" + IconSkipPrevious IconName = "skipPrevious" + IconStar IconName = "star" + IconStarHalf IconName = "starHalf" + IconStarOff IconName = "starOff" + IconStop IconName = "stop" + IconUpload IconName = "upload" + IconVisibility IconName = "visibility" + IconVisibilityOff IconName = "visibilityOff" + IconVolumeDown IconName = "volumeDown" + IconVolumeMute IconName = "volumeMute" + IconVolumeOff IconName = "volumeOff" + IconVolumeUp IconName = "volumeUp" + IconWarning IconName = "warning" +) diff --git a/agent_sdks/go/a2ui/v09/zz_wrapper.go b/agent_sdks/go/a2ui/v09/zz_wrapper.go new file mode 100644 index 0000000000..7d8b5d261d --- /dev/null +++ b/agent_sdks/go/a2ui/v09/zz_wrapper.go @@ -0,0 +1,15 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v09 + +// ClientMessageListWrapper wraps a list of [ClientMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ClientMessageListWrapper struct { + Messages []ClientMessage `json:"messages"` +} + +// ServerMessageListWrapper wraps a list of [ServerMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ServerMessageListWrapper struct { + Messages []ServerMessage `json:"messages"` +} diff --git a/agent_sdks/go/a2ui/v091/capabilities.go b/agent_sdks/go/a2ui/v091/capabilities.go new file mode 100644 index 0000000000..05fe7d2c1a --- /dev/null +++ b/agent_sdks/go/a2ui/v091/capabilities.go @@ -0,0 +1,50 @@ +package v091 + +import "encoding/json" + +// ClientCapabilities describes a client's UI rendering capabilities, +// sent as part of A2A metadata. +type ClientCapabilities struct { + V091 *ClientCapabilitiesV091 `json:"v0.9.1,omitempty"` +} + +// ClientCapabilitiesV091 is the v0.9.1 client capabilities structure. +type ClientCapabilitiesV091 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds"` + InlineCatalogs []CatalogDef `json:"inlineCatalogs,omitempty"` +} + +// ServerCapabilities describes an agent's supported UI features, +// advertised via agent card or other discovery. +type ServerCapabilities struct { + V091 *ServerCapabilitiesV091 `json:"v0.9.1,omitempty"` +} + +// ServerCapabilitiesV091 is the v0.9.1 server capabilities structure. +type ServerCapabilitiesV091 struct { + SupportedCatalogIDs []string `json:"supportedCatalogIds,omitempty"` + AcceptsInlineCatalogs bool `json:"acceptsInlineCatalogs,omitempty"` +} + +// CatalogDef is an inline catalog definition containing component schemas +// and function definitions. +type CatalogDef struct { + CatalogID string `json:"catalogId"` + Components map[string]json.RawMessage `json:"components,omitempty"` + Functions []FunctionDefinition `json:"functions,omitempty"` + Theme map[string]json.RawMessage `json:"theme,omitempty"` +} + +// FunctionDefinition describes a function's interface for catalog definitions. +type FunctionDefinition struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Parameters json.RawMessage `json:"parameters"` + ReturnType ReturnType `json:"returnType"` +} + +// ClientDataModel carries the client data model in A2A message metadata. +type ClientDataModel struct { + Version string `json:"version"` + Surfaces map[string]map[string]any `json:"surfaces"` +} diff --git a/agent_sdks/go/a2ui/v091/common.go b/agent_sdks/go/a2ui/v091/common.go new file mode 100644 index 0000000000..3340e09171 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/common.go @@ -0,0 +1,67 @@ +package v091 + +// DataBinding references a value in the client data model by JSON Pointer path. +type DataBinding struct { + Path string `json:"path"` +} + +// FunctionCall invokes a named client-side function. +type FunctionCall struct { + Call string `json:"call"` + Args map[string]any `json:"args,omitempty"` + ReturnType ReturnType `json:"returnType,omitempty"` +} + +// ChildList is either a static list of component IDs or a dynamic template. +// Exactly one of IDs or Template is set. +type ChildList struct { + IDs []string + Template *ChildTemplate +} + +// ChildTemplate generates a dynamic list of children from a data model list. +type ChildTemplate struct { + ComponentID string `json:"componentId"` + Path string `json:"path"` +} + +// CheckRule is a single validation rule applied to an input component. +type CheckRule struct { + Condition DynamicBoolean `json:"condition"` + Message string `json:"message"` +} + +// AccessibilityAttributes enhance accessibility for assistive technologies. +type AccessibilityAttributes struct { + Label *DynamicString `json:"label,omitempty"` + Description *DynamicString `json:"description,omitempty"` +} + +// Theme defines visual theming for a surface. +type Theme struct { + PrimaryColor string `json:"primaryColor,omitempty"` + IconURL string `json:"iconUrl,omitempty"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AdditionalProperties map[string]any `json:"-"` +} + +// Action is an interaction handler that either triggers a server-side event +// or executes a client-side function. Exactly one field is non-nil. +type Action struct { + Event *EventAction `json:"event,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` +} + +// EventAction triggers a server-side event. +type EventAction struct { + Name string `json:"name"` + Context map[string]DynamicValue `json:"context,omitempty"` +} + +// IconNameOrPath is a well-known icon name, custom SVG path, or binding. +// Exactly one field is non-nil. +type IconNameOrPath struct { + Name *IconName + SVGPath *string + Binding *DataBinding +} diff --git a/agent_sdks/go/a2ui/v091/common_json.go b/agent_sdks/go/a2ui/v091/common_json.go new file mode 100644 index 0000000000..5ece646298 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/common_json.go @@ -0,0 +1,183 @@ +package v091 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for Theme. +func (t Theme) MarshalJSON() ([]byte, error) { + fields := make(map[string]any, len(t.AdditionalProperties)+3) + for k, v := range t.AdditionalProperties { + fields[k] = v + } + if t.PrimaryColor != "" { + fields["primaryColor"] = t.PrimaryColor + } + if t.IconURL != "" { + fields["iconUrl"] = t.IconURL + } + if t.AgentDisplayName != "" { + fields["agentDisplayName"] = t.AgentDisplayName + } + return json.Marshal(fields) +} + +// UnmarshalJSON implements json.Unmarshaler for Theme. +func (t *Theme) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return fmt.Errorf("a2ui: unmarshal theme: %w", err) + } + *t = Theme{} + for key, raw := range fields { + switch key { + case "primaryColor": + if err := json.Unmarshal(raw, &t.PrimaryColor); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.primaryColor: %w", err) + } + case "iconUrl": + if err := json.Unmarshal(raw, &t.IconURL); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.iconUrl: %w", err) + } + case "agentDisplayName": + if err := json.Unmarshal(raw, &t.AgentDisplayName); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.agentDisplayName: %w", err) + } + default: + if t.AdditionalProperties == nil { + t.AdditionalProperties = make(map[string]any) + } + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return fmt.Errorf("a2ui: unmarshal theme.%s: %w", key, err) + } + t.AdditionalProperties[key] = value + } + } + return nil +} + +// MarshalJSON implements json.Marshaler for ChildList. +func (c ChildList) MarshalJSON() ([]byte, error) { + if c.Template != nil && c.IDs != nil { + return nil, fmt.Errorf("a2ui: ChildList has both ids and template set") + } + switch { + case c.Template != nil: + return json.Marshal(c.Template) + case c.IDs != nil: + return json.Marshal(c.IDs) + default: + return []byte("[]"), nil + } +} + +// UnmarshalJSON implements json.Unmarshaler for ChildList. +func (c *ChildList) UnmarshalJSON(data []byte) error { + *c = ChildList{} + var ids []string + if err := json.Unmarshal(data, &ids); err == nil { + c.IDs = ids + return nil + } + var t ChildTemplate + if err := json.Unmarshal(data, &t); err != nil { + return fmt.Errorf("a2ui: unmarshal child list: %w", err) + } + c.Template = &t + return nil +} + +// MarshalJSON implements json.Marshaler for Action. +func (a Action) MarshalJSON() ([]byte, error) { + type actionAlias Action + switch countSet(a.Event != nil, a.FunctionCall != nil) { + case 1: + return json.Marshal(actionAlias(a)) + case 0: + return nil, fmt.Errorf("a2ui: Action has no value set") + default: + return nil, fmt.Errorf("a2ui: Action has multiple values set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for Action. +func (a *Action) UnmarshalJSON(data []byte) error { + type actionAlias Action + var aa actionAlias + if err := json.Unmarshal(data, &aa); err != nil { + return fmt.Errorf("a2ui: unmarshal action: %w", err) + } + switch countSet(aa.Event != nil, aa.FunctionCall != nil) { + case 1: + *a = Action(aa) + return nil + case 0: + return fmt.Errorf("a2ui: action must have event or functionCall") + default: + return fmt.Errorf("a2ui: action must not have both event and functionCall") + } +} + +// MarshalJSON implements json.Marshaler for IconNameOrPath. +func (i IconNameOrPath) MarshalJSON() ([]byte, error) { + switch countSet(i.Name != nil, i.SVGPath != nil, i.Binding != nil) { + case 1: + switch { + case i.Name != nil: + return json.Marshal(string(*i.Name)) + case i.SVGPath != nil: + return json.Marshal(struct { + SVGPath string `json:"svgPath"` + }{SVGPath: *i.SVGPath}) + case i.Binding != nil: + return json.Marshal(i.Binding) + } + case 0: + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") + default: + return nil, fmt.Errorf("a2ui: IconNameOrPath has multiple values set") + } + return nil, fmt.Errorf("a2ui: IconNameOrPath has no value set") +} + +// UnmarshalJSON implements json.Unmarshaler for IconNameOrPath. +func (i *IconNameOrPath) UnmarshalJSON(data []byte) error { + *i = IconNameOrPath{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + name := IconName(s) + i.Name = &name + return nil + } + var obj struct { + SVGPath string `json:"svgPath"` + Path string `json:"path"` + } + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: unmarshal icon name or path: %w", err) + } + switch { + case obj.SVGPath != "" && obj.Path != "": + return fmt.Errorf("a2ui: icon name must not have both svgPath and path") + case obj.SVGPath != "": + i.SVGPath = &obj.SVGPath + return nil + case obj.Path != "": + i.Binding = &DataBinding{Path: obj.Path} + return nil + default: + return fmt.Errorf("a2ui: icon path must not be empty") + } +} + +func countSet(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} diff --git a/agent_sdks/go/a2ui/v091/common_test.go b/agent_sdks/go/a2ui/v091/common_test.go new file mode 100644 index 0000000000..4c0a3525f1 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/common_test.go @@ -0,0 +1,51 @@ +package v091 + +import ( + "encoding/json" + "testing" +) + +func TestActionRejectsInvalidStates(t *testing.T) { + action := Action{ + Event: &EventAction{Name: "submit"}, + FunctionCall: &FunctionCall{Call: "openUrl"}, + } + if _, err := json.Marshal(action); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded Action + if err := json.Unmarshal([]byte(`{"event":{"name":"submit"},"functionCall":{"call":"openUrl"}}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestIconNameOrPathRejectsInvalidStates(t *testing.T) { + path := "/tmp/icon.svg" + name := IconSearch + icon := IconNameOrPath{Name: &name, SVGPath: &path} + if _, err := json.Marshal(icon); err == nil { + t.Fatal("expected marshal error, got nil") + } + + var decoded IconNameOrPath + if err := json.Unmarshal([]byte(`{"svgPath":""}`), &decoded); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +func TestIconNameOrPathV091Forms(t *testing.T) { + path := "M0 0h1v1z" + roundTrip(t, IconNameOrPath{SVGPath: &path}, `{"svgPath":"M0 0h1v1z"}`) + roundTrip(t, IconNameOrPath{Binding: &DataBinding{Path: "/icon"}}, `{"path":"/icon"}`) +} + +func TestChildListRejectsMultipleRepresentations(t *testing.T) { + children := ChildList{ + IDs: []string{"a"}, + Template: &ChildTemplate{ComponentID: "child", Path: "/items"}, + } + if _, err := json.Marshal(children); err == nil { + t.Fatal("expected marshal error, got nil") + } +} diff --git a/agent_sdks/go/a2ui/v091/component.go b/agent_sdks/go/a2ui/v091/component.go new file mode 100644 index 0000000000..60c0a4686c --- /dev/null +++ b/agent_sdks/go/a2ui/v091/component.go @@ -0,0 +1,110 @@ +package v091 + +// Component represents any A2UI component in the component tree. +// Exactly one of the concrete type fields is non-nil. +// +// MarshalJSON/UnmarshalJSON in zz_component_marshal.go handle +// serialization, using the "component" field as a discriminator. +type Component struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + + // Concrete type fields (exactly one non-nil). + Text *TextComponent `json:"-"` + Image *ImageComponent `json:"-"` + Icon *IconComponent `json:"-"` + Video *VideoComponent `json:"-"` + AudioPlayer *AudioPlayerComponent `json:"-"` + Row *RowComponent `json:"-"` + Column *ColumnComponent `json:"-"` + List *ListComponent `json:"-"` + Card *CardComponent `json:"-"` + Tabs *TabsComponent `json:"-"` + Modal *ModalComponent `json:"-"` + Divider *DividerComponent `json:"-"` + Button *ButtonComponent `json:"-"` + TextField *TextFieldComponent `json:"-"` + CheckBox *CheckBoxComponent `json:"-"` + ChoicePicker *ChoicePickerComponent `json:"-"` + Slider *SliderComponent `json:"-"` + DateTimeInput *DateTimeInputComponent `json:"-"` +} + +func (c Component) componentData() (string, any, int) { + var ( + componentType string + specific any + count int + ) + set := func(typ string, value any) { + componentType = typ + specific = value + count++ + } + if c.Text != nil { + set("Text", c.Text) + } + if c.Image != nil { + set("Image", c.Image) + } + if c.Icon != nil { + set("Icon", c.Icon) + } + if c.Video != nil { + set("Video", c.Video) + } + if c.AudioPlayer != nil { + set("AudioPlayer", c.AudioPlayer) + } + if c.Row != nil { + set("Row", c.Row) + } + if c.Column != nil { + set("Column", c.Column) + } + if c.List != nil { + set("List", c.List) + } + if c.Card != nil { + set("Card", c.Card) + } + if c.Tabs != nil { + set("Tabs", c.Tabs) + } + if c.Modal != nil { + set("Modal", c.Modal) + } + if c.Divider != nil { + set("Divider", c.Divider) + } + if c.Button != nil { + set("Button", c.Button) + } + if c.TextField != nil { + set("TextField", c.TextField) + } + if c.CheckBox != nil { + set("CheckBox", c.CheckBox) + } + if c.ChoicePicker != nil { + set("ChoicePicker", c.ChoicePicker) + } + if c.Slider != nil { + set("Slider", c.Slider) + } + if c.DateTimeInput != nil { + set("DateTimeInput", c.DateTimeInput) + } + return componentType, specific, count +} + +// ComponentType returns the discriminator string (e.g. "Text", "Button"). +func (c Component) ComponentType() string { + componentType, _, count := c.componentData() + if count != 1 { + return "" + } + return componentType +} diff --git a/agent_sdks/go/a2ui/v091/component_test.go b/agent_sdks/go/a2ui/v091/component_test.go new file mode 100644 index 0000000000..4b0e8d8531 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/component_test.go @@ -0,0 +1,254 @@ +package v091 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +const basicExamplesDir = "testdata/v0_9_1/catalogs/basic/examples" + +func TestLoginFormComponents(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/09_login-form.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + + // Second message is updateComponents. + var msg ServerMessage + if err := json.Unmarshal(example.Messages[1], &msg); err != nil { + t.Fatal(err) + } + if msg.UpdateComponents == nil { + t.Fatal("expected updateComponents") + } + + components := msg.UpdateComponents.Components + byID := make(map[string]*Component, len(components)) + for i := range components { + byID[components[i].ID] = &components[i] + } + + t.Run("TextField", func(t *testing.T) { + c, ok := byID["email-field"] + if !ok { + t.Fatal("missing email-field") + } + if c.ComponentType() != "TextField" { + t.Fatalf("type = %q, want TextField", c.ComponentType()) + } + if c.TextField == nil { + t.Fatal("TextField is nil") + } + if c.TextField.Label.Literal == nil || *c.TextField.Label.Literal != "Email" { + t.Fatalf("label = %+v, want literal Email", c.TextField.Label) + } + if c.TextField.Value == nil || c.TextField.Value.Binding == nil || c.TextField.Value.Binding.Path != "/email" { + t.Fatal("value should bind to /email") + } + }) + + t.Run("Button", func(t *testing.T) { + c, ok := byID["login-btn"] + if !ok { + t.Fatal("missing login-btn") + } + if c.ComponentType() != "Button" { + t.Fatalf("type = %q, want Button", c.ComponentType()) + } + if c.Button.Child != "login-btn-text" { + t.Fatalf("child = %q", c.Button.Child) + } + if c.Button.Action.Event == nil { + t.Fatal("expected event action") + } + if c.Button.Action.Event.Name != "login" { + t.Fatalf("event name = %q", c.Button.Action.Event.Name) + } + }) + + t.Run("Column", func(t *testing.T) { + c, ok := byID["main-column"] + if !ok { + t.Fatal("missing main-column") + } + if c.ComponentType() != "Column" { + t.Fatalf("type = %q, want Column", c.ComponentType()) + } + if len(c.Column.Children.IDs) != 6 { + t.Fatalf("children = %d, want 6", len(c.Column.Children.IDs)) + } + }) + + t.Run("CheckRule", func(t *testing.T) { + c := byID["email-field"] + if len(c.Checks) != 2 { + t.Fatalf("checks = %d, want 2", len(c.Checks)) + } + if c.Checks[0].Message != "Email is required" { + t.Fatalf("message = %q", c.Checks[0].Message) + } + if c.Checks[0].Condition.FunctionCall == nil { + t.Fatal("expected function call condition") + } + if c.Checks[0].Condition.FunctionCall.Call != "required" { + t.Fatalf("call = %q", c.Checks[0].Condition.FunctionCall.Call) + } + }) + + t.Run("Card", func(t *testing.T) { + c, ok := byID["root"] + if !ok { + t.Fatal("missing root") + } + if c.ComponentType() != "Card" { + t.Fatalf("type = %q, want Card", c.ComponentType()) + } + if c.Card.Child != "main-column" { + t.Fatalf("child = %q", c.Card.Child) + } + }) +} + +func TestComponentTypeDiscriminator(t *testing.T) { + tests := []struct { + name string + comp Component + want string + }{ + {"Text", Component{Text: &TextComponent{}}, "Text"}, + {"Button", Component{Button: &ButtonComponent{}}, "Button"}, + {"Column", Component{Column: &ColumnComponent{}}, "Column"}, + {"Row", Component{Row: &RowComponent{}}, "Row"}, + {"Card", Component{Card: &CardComponent{}}, "Card"}, + {"Image", Component{Image: &ImageComponent{}}, "Image"}, + {"Icon", Component{Icon: &IconComponent{}}, "Icon"}, + {"TextField", Component{TextField: &TextFieldComponent{}}, "TextField"}, + {"CheckBox", Component{CheckBox: &CheckBoxComponent{}}, "CheckBox"}, + {"Divider", Component{Divider: &DividerComponent{}}, "Divider"}, + {"Slider", Component{Slider: &SliderComponent{}}, "Slider"}, + {"Tabs", Component{Tabs: &TabsComponent{}}, "Tabs"}, + {"Modal", Component{Modal: &ModalComponent{}}, "Modal"}, + {"List", Component{List: &ListComponent{}}, "List"}, + {"multiple", Component{Text: &TextComponent{}, Button: &ButtonComponent{}}, ""}, + {"empty", Component{}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.comp.ComponentType(); got != tt.want { + t.Fatalf("ComponentType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestComponentRoundTrip(t *testing.T) { + tests := []struct { + name string + json string + }{ + { + name: "text", + json: `{"component":"Text","id":"t1","text":"hello","variant":"h1"}`, + }, + { + name: "button_with_event", + json: `{"component":"Button","id":"b1","child":"b1-text","action":{"event":{"name":"click"}}}`, + }, + { + name: "column", + json: `{"component":"Column","id":"c1","children":["a","b","c"],"align":"center"}`, + }, + { + name: "text_with_binding", + json: `{"component":"Text","id":"t2","text":{"path":"/name"}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var c Component + if err := json.Unmarshal([]byte(tt.json), &c); err != nil { + t.Fatalf("unmarshal: %v", err) + } + roundTrip(t, c, tt.json) + }) + } +} + +func TestComponentMarshalRejectsInvalidConcreteTypes(t *testing.T) { + tests := []struct { + name string + comp Component + }{ + { + name: "none", + comp: Component{ID: "empty"}, + }, + { + name: "multiple", + comp: Component{ + ID: "bad", + Text: &TextComponent{Text: StringLiteral("hello")}, + Button: &ButtonComponent{Action: Action{Event: &EventAction{Name: "click"}}, Child: "child"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.comp); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestAllExamplesUnmarshal(t *testing.T) { + entries, err := os.ReadDir(basicExamplesDir) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + t.Run(e.Name(), func(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/" + e.Name()) + if err != nil { + t.Fatal(err) + } + var example struct { + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + for i, raw := range example.Messages { + var msg ServerMessage + if err := json.Unmarshal(raw, &msg); err != nil { + t.Fatalf("message[%d]: unmarshal: %v", i, err) + } + remarshaled, err := json.Marshal(msg) + if err != nil { + t.Fatalf("message[%d]: re-marshal: %v", i, err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("message[%d]: unmarshal re-marshaled: %v", i, err) + } + if err := json.Unmarshal(raw, &want); err != nil { + t.Fatalf("message[%d]: unmarshal original: %v", i, err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("message[%d]: round-trip mismatch", i) + } + } + }) + } +} diff --git a/agent_sdks/go/a2ui/v091/doc.go b/agent_sdks/go/a2ui/v091/doc.go new file mode 100644 index 0000000000..b99c97312f --- /dev/null +++ b/agent_sdks/go/a2ui/v091/doc.go @@ -0,0 +1,2 @@ +// Package v091 provides Go types for the A2UI v0.9.1 extension, whose JSON message version is v0.9. +package v091 diff --git a/agent_sdks/go/a2ui/v091/dynamic.go b/agent_sdks/go/a2ui/v091/dynamic.go new file mode 100644 index 0000000000..30a6d3d0c5 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/dynamic.go @@ -0,0 +1,120 @@ +package v091 + +// DynamicString represents a string that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicString struct { + Literal *string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringLiteral creates a DynamicString from a literal string value. +func StringLiteral(s string) DynamicString { return DynamicString{Literal: &s} } + +// StringBinding creates a DynamicString from a data model path. +func StringBinding(path string) DynamicString { + return DynamicString{Binding: &DataBinding{Path: path}} +} + +// StringFunc creates a DynamicString from a function call. +func StringFunc(call FunctionCall) DynamicString { + return DynamicString{FunctionCall: &call} +} + +// DynamicNumber represents a number that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicNumber struct { + Literal *float64 + Binding *DataBinding + FunctionCall *FunctionCall +} + +// NumberLiteral creates a DynamicNumber from a literal float64 value. +func NumberLiteral(n float64) DynamicNumber { return DynamicNumber{Literal: &n} } + +// NumberBinding creates a DynamicNumber from a data model path. +func NumberBinding(path string) DynamicNumber { + return DynamicNumber{Binding: &DataBinding{Path: path}} +} + +// NumberFunc creates a DynamicNumber from a function call. +func NumberFunc(call FunctionCall) DynamicNumber { + return DynamicNumber{FunctionCall: &call} +} + +// DynamicBoolean represents a boolean that can be a literal, a data binding, +// or a function call. Exactly one field is non-nil. +type DynamicBoolean struct { + Literal *bool + Binding *DataBinding + FunctionCall *FunctionCall +} + +// BoolLiteral creates a DynamicBoolean from a literal bool value. +func BoolLiteral(b bool) DynamicBoolean { return DynamicBoolean{Literal: &b} } + +// BoolBinding creates a DynamicBoolean from a data model path. +func BoolBinding(path string) DynamicBoolean { + return DynamicBoolean{Binding: &DataBinding{Path: path}} +} + +// BoolFunc creates a DynamicBoolean from a function call. +func BoolFunc(call FunctionCall) DynamicBoolean { + return DynamicBoolean{FunctionCall: &call} +} + +// DynamicStringList represents a string list that can be a literal, a data +// binding, or a function call. Exactly one field is non-nil. +type DynamicStringList struct { + Literal []string + Binding *DataBinding + FunctionCall *FunctionCall +} + +// StringListLiteral creates a DynamicStringList from literal string values. +func StringListLiteral(ss []string) DynamicStringList { + return DynamicStringList{Literal: ss} +} + +// StringListBinding creates a DynamicStringList from a data model path. +func StringListBinding(path string) DynamicStringList { + return DynamicStringList{Binding: &DataBinding{Path: path}} +} + +// StringListFunc creates a DynamicStringList from a function call. +func StringListFunc(call FunctionCall) DynamicStringList { + return DynamicStringList{FunctionCall: &call} +} + +// DynamicValue represents a value of any type: string, number, boolean, array, +// data binding, or function call. Exactly one field is non-nil. +type DynamicValue struct { + String *string + Number *float64 + Bool *bool + Array []any + Binding *DataBinding + FunctionCall *FunctionCall +} + +// ValueString creates a DynamicValue from a string. +func ValueString(s string) DynamicValue { return DynamicValue{String: &s} } + +// ValueNumber creates a DynamicValue from a number. +func ValueNumber(n float64) DynamicValue { return DynamicValue{Number: &n} } + +// ValueBool creates a DynamicValue from a boolean. +func ValueBool(b bool) DynamicValue { return DynamicValue{Bool: &b} } + +// ValueArray creates a DynamicValue from an array. +func ValueArray(a []any) DynamicValue { return DynamicValue{Array: a} } + +// ValueBinding creates a DynamicValue from a data model path. +func ValueBinding(path string) DynamicValue { + return DynamicValue{Binding: &DataBinding{Path: path}} +} + +// ValueFunc creates a DynamicValue from a function call. +func ValueFunc(call FunctionCall) DynamicValue { + return DynamicValue{FunctionCall: &call} +} diff --git a/agent_sdks/go/a2ui/v091/dynamic_json.go b/agent_sdks/go/a2ui/v091/dynamic_json.go new file mode 100644 index 0000000000..d19af7c60b --- /dev/null +++ b/agent_sdks/go/a2ui/v091/dynamic_json.go @@ -0,0 +1,239 @@ +package v091 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for DynamicString. +func (d DynamicString) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicString has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicString has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicString. +func (d *DynamicString) UnmarshalJSON(data []byte) error { + *d = DynamicString{} + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.Literal = &s + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicNumber. +func (d DynamicNumber) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicNumber has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicNumber has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicNumber. +func (d *DynamicNumber) UnmarshalJSON(data []byte) error { + *d = DynamicNumber{} + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Literal = &n + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicBoolean. +func (d DynamicBoolean) MarshalJSON() ([]byte, error) { + if count := countSet(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicBoolean has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(*d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicBoolean has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicBoolean. +func (d *DynamicBoolean) UnmarshalJSON(data []byte) error { + *d = DynamicBoolean{} + var b bool + if err := json.Unmarshal(data, &b); err == nil { + d.Literal = &b + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicStringList. +func (d DynamicStringList) MarshalJSON() ([]byte, error) { + if count := countSliceValues(d.Literal != nil, d.Binding != nil, d.FunctionCall != nil); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicStringList has multiple values set") + } + switch { + case d.Literal != nil: + return json.Marshal(d.Literal) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicStringList has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicStringList. +func (d *DynamicStringList) UnmarshalJSON(data []byte) error { + *d = DynamicStringList{} + var ss []string + if err := json.Unmarshal(data, &ss); err == nil { + d.Literal = ss + return nil + } + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// MarshalJSON implements json.Marshaler for DynamicValue. +func (d DynamicValue) MarshalJSON() ([]byte, error) { + if count := countDynamicValueFields(d); count > 1 { + return nil, fmt.Errorf("a2ui: DynamicValue has multiple values set") + } + switch { + case d.String != nil: + return json.Marshal(*d.String) + case d.Number != nil: + return json.Marshal(*d.Number) + case d.Bool != nil: + return json.Marshal(*d.Bool) + case d.Array != nil: + return json.Marshal(d.Array) + case d.Binding != nil: + return json.Marshal(d.Binding) + case d.FunctionCall != nil: + return json.Marshal(d.FunctionCall) + default: + return nil, fmt.Errorf("a2ui: DynamicValue has no value set") + } +} + +// UnmarshalJSON implements json.Unmarshaler for DynamicValue. +func (d *DynamicValue) UnmarshalJSON(data []byte) error { + *d = DynamicValue{} + data = bytes.TrimSpace(data) + // Try string. + var s string + if err := json.Unmarshal(data, &s); err == nil { + d.String = &s + return nil + } + // Try bool (before number, since Go's json decoder doesn't confuse them, + // but we check bool first for clarity). + var b bool + if err := json.Unmarshal(data, &b); err == nil { + if len(data) > 0 && (data[0] == 't' || data[0] == 'f') { + d.Bool = &b + return nil + } + } + // Try number. + var n float64 + if err := json.Unmarshal(data, &n); err == nil { + d.Number = &n + return nil + } + // Try array. + var arr []any + if err := json.Unmarshal(data, &arr); err == nil { + d.Array = arr + return nil + } + // Must be an object: binding or function call. + return unmarshalBindingOrFunc(data, &d.Binding, &d.FunctionCall) +} + +// unmarshalBindingOrFunc tries to unmarshal data as a DataBinding (has "path" +// key) or a FunctionCall (has "call" key). +func unmarshalBindingOrFunc(data []byte, binding **DataBinding, fn **FunctionCall) error { + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("a2ui: cannot unmarshal dynamic value: %w", err) + } + if _, ok := obj["path"]; ok { + if _, ok := obj["call"]; ok { + return fmt.Errorf("a2ui: object cannot be both a data binding and a function call") + } + var db DataBinding + if err := json.Unmarshal(data, &db); err != nil { + return fmt.Errorf("a2ui: unmarshal data binding: %w", err) + } + *binding = &db + return nil + } + if _, ok := obj["call"]; ok { + var fc FunctionCall + if err := json.Unmarshal(data, &fc); err != nil { + return fmt.Errorf("a2ui: unmarshal function call: %w", err) + } + *fn = &fc + return nil + } + return fmt.Errorf("a2ui: object is neither a data binding nor a function call") +} + +func countSliceValues(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} + +func countDynamicValueFields(d DynamicValue) int { + var count int + if d.String != nil { + count++ + } + if d.Number != nil { + count++ + } + if d.Bool != nil { + count++ + } + if d.Array != nil { + count++ + } + if d.Binding != nil { + count++ + } + if d.FunctionCall != nil { + count++ + } + return count +} diff --git a/agent_sdks/go/a2ui/v091/dynamic_test.go b/agent_sdks/go/a2ui/v091/dynamic_test.go new file mode 100644 index 0000000000..bd9e366f66 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/dynamic_test.go @@ -0,0 +1,308 @@ +package v091 + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestDynamicString(t *testing.T) { + tests := []struct { + name string + json string + want DynamicString + }{ + { + name: "literal", + json: `"hello"`, + want: StringLiteral("hello"), + }, + { + name: "binding", + json: `{"path":"/foo"}`, + want: StringBinding("/foo"), + }, + { + name: "function_call", + json: `{"call":"formatString","args":{"value":"hi"},"returnType":"string"}`, + want: StringFunc(FunctionCall{ + Call: "formatString", + Args: map[string]any{"value": "hi"}, + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicNumber(t *testing.T) { + tests := []struct { + name string + json string + want DynamicNumber + }{ + { + name: "literal", + json: `42.5`, + want: NumberLiteral(42.5), + }, + { + name: "integer", + json: `100`, + want: NumberLiteral(100), + }, + { + name: "binding", + json: `{"path":"/count"}`, + want: NumberBinding("/count"), + }, + { + name: "function_call", + json: `{"call":"add","args":{"a":1,"b":2},"returnType":"number"}`, + want: NumberFunc(FunctionCall{ + Call: "add", + Args: map[string]any{"a": float64(1), "b": float64(2)}, + ReturnType: ReturnTypeNumber, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicNumber + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicBoolean(t *testing.T) { + tests := []struct { + name string + json string + want DynamicBoolean + }{ + { + name: "true", + json: `true`, + want: BoolLiteral(true), + }, + { + name: "false", + json: `false`, + want: BoolLiteral(false), + }, + { + name: "binding", + json: `{"path":"/enabled"}`, + want: BoolBinding("/enabled"), + }, + { + name: "function_call", + json: `{"call":"required","args":{"value":"x"},"returnType":"boolean"}`, + want: BoolFunc(FunctionCall{ + Call: "required", + Args: map[string]any{"value": "x"}, + ReturnType: ReturnTypeBoolean, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicBoolean + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicStringList(t *testing.T) { + tests := []struct { + name string + json string + want DynamicStringList + }{ + { + name: "literal", + json: `["a","b"]`, + want: StringListLiteral([]string{"a", "b"}), + }, + { + name: "binding", + json: `{"path":"/tags"}`, + want: StringListBinding("/tags"), + }, + { + name: "function_call", + json: `{"call":"split","args":{"sep":","},"returnType":"array"}`, + want: StringListFunc(FunctionCall{ + Call: "split", + Args: map[string]any{"sep": ","}, + ReturnType: ReturnTypeArray, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicStringList + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicValue(t *testing.T) { + tests := []struct { + name string + json string + want DynamicValue + }{ + { + name: "string", + json: `"hello"`, + want: ValueString("hello"), + }, + { + name: "number", + json: `3.14`, + want: ValueNumber(3.14), + }, + { + name: "bool_true", + json: `true`, + want: ValueBool(true), + }, + { + name: "bool_false", + json: `false`, + want: ValueBool(false), + }, + { + name: "array", + json: `[1,"two",true]`, + want: ValueArray([]any{float64(1), "two", true}), + }, + { + name: "binding", + json: `{"path":"/data"}`, + want: ValueBinding("/data"), + }, + { + name: "function_call", + json: `{"call":"now","returnType":"string"}`, + want: ValueFunc(FunctionCall{ + Call: "now", + ReturnType: ReturnTypeString, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got DynamicValue + if err := json.Unmarshal([]byte(tt.json), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + roundTrip(t, got, tt.json) + }) + } +} + +func TestDynamicMarshalEmpty(t *testing.T) { + tests := []struct { + name string + fn func() ([]byte, error) + }{ + {"DynamicString", func() ([]byte, error) { return json.Marshal(DynamicString{}) }}, + {"DynamicNumber", func() ([]byte, error) { return json.Marshal(DynamicNumber{}) }}, + {"DynamicBoolean", func() ([]byte, error) { return json.Marshal(DynamicBoolean{}) }}, + {"DynamicStringList", func() ([]byte, error) { return json.Marshal(DynamicStringList{}) }}, + {"DynamicValue", func() ([]byte, error) { return json.Marshal(DynamicValue{}) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.fn() + if err == nil { + t.Fatal("expected error for zero-value marshal, got nil") + } + }) + } +} + +func TestDynamicRejectsMultipleValues(t *testing.T) { + s := "hello" + tests := []struct { + name string + value any + }{ + {"DynamicString", DynamicString{Literal: &s, Binding: &DataBinding{Path: "/name"}}}, + {"DynamicNumber", DynamicNumber{Literal: float64Ptr(42), Binding: &DataBinding{Path: "/count"}}}, + {"DynamicBoolean", DynamicBoolean{Literal: boolPtr(true), Binding: &DataBinding{Path: "/enabled"}}}, + {"DynamicStringList", DynamicStringList{Literal: []string{"a"}, Binding: &DataBinding{Path: "/tags"}}}, + {"DynamicValue", DynamicValue{String: &s, Binding: &DataBinding{Path: "/value"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.value); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } +} + +func TestDynamicRejectsAmbiguousObject(t *testing.T) { + var got DynamicString + if err := json.Unmarshal([]byte(`{"path":"/name","call":"formatString"}`), &got); err == nil { + t.Fatal("expected unmarshal error, got nil") + } +} + +// roundTrip marshals v, then verifies the JSON is semantically equivalent to wantJSON. +func roundTrip(t *testing.T, v any, wantJSON string) { + t.Helper() + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal marshaled: %v", err) + } + if err := json.Unmarshal([]byte(wantJSON), &want); err != nil { + t.Fatalf("unmarshal want: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round-trip mismatch:\n got: %s\n want: %s", data, wantJSON) + } +} + +func boolPtr(v bool) *bool { return &v } + +func float64Ptr(v float64) *float64 { return &v } diff --git a/agent_sdks/go/a2ui/v091/example_test.go b/agent_sdks/go/a2ui/v091/example_test.go new file mode 100644 index 0000000000..5cc42d4743 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/example_test.go @@ -0,0 +1,63 @@ +package v091_test + +import ( + "encoding/json" + "fmt" + + v091 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v091" +) + +func Example() { + msg := v091.ServerMessage{ + Version: v091.Version, + CreateSurface: &v091.CreateSurface{ + SurfaceID: "demo", + CatalogID: "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json", + }, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) + // Output: {"version":"v0.9","createSurface":{"surfaceId":"demo","catalogId":"https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"}} +} + +func ExampleComponent() { + comp := v091.Component{ + ID: "greeting", + Text: &v091.TextComponent{ + Text: v091.StringLiteral("Hello, world!"), + Variant: v091.TextVariantH1, + }, + } + data, _ := json.Marshal(comp) + fmt.Println(string(data)) + // Output: {"component":"Text","id":"greeting","text":"Hello, world!","variant":"h1"} +} + +func ExampleDynamicString() { + // Literal string. + lit := v091.StringLiteral("hello") + data, _ := json.Marshal(lit) + fmt.Println(string(data)) + + // Data binding. + bind := v091.StringBinding("/user/name") + data, _ = json.Marshal(bind) + fmt.Println(string(data)) + // Output: + // "hello" + // {"path":"/user/name"} +} + +func ExampleDynamicNumber() { + n := v091.NumberLiteral(42) + data, _ := json.Marshal(n) + fmt.Println(string(data)) + // Output: 42 +} + +func ExampleDynamicBoolean() { + b := v091.BoolBinding("/settings/enabled") + data, _ := json.Marshal(b) + fmt.Println(string(data)) + // Output: {"path":"/settings/enabled"} +} diff --git a/agent_sdks/go/a2ui/v091/gen.go b/agent_sdks/go/a2ui/v091/gen.go new file mode 100644 index 0000000000..70a068f746 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/gen.go @@ -0,0 +1,3 @@ +package v091 + +//go:generate go run ../../cmd/a2uigen -schemas=../../../../specification/v0_9_1/json -pkg=v091 -out=../.. diff --git a/agent_sdks/go/a2ui/v091/message.go b/agent_sdks/go/a2ui/v091/message.go new file mode 100644 index 0000000000..ba37ff44c9 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/message.go @@ -0,0 +1,71 @@ +package v091 + +// Version is the A2UI wire protocol version implemented by this package. +const Version = "v0.9" + +// ServerMessage is a message sent from the agent to the renderer. +// Exactly one of the payload fields is non-nil. +type ServerMessage struct { + Version string `json:"version"` + CreateSurface *CreateSurface `json:"createSurface,omitempty"` + UpdateComponents *UpdateComponents `json:"updateComponents,omitempty"` + UpdateDataModel *UpdateDataModel `json:"updateDataModel,omitempty"` + DeleteSurface *DeleteSurface `json:"deleteSurface,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ServerMessage) VersionString() string { return m.Version } + +// CreateSurface signals the client to create a new surface. +type CreateSurface struct { + SurfaceID string `json:"surfaceId"` + CatalogID string `json:"catalogId"` + Theme *Theme `json:"theme,omitempty"` + SendDataModel bool `json:"sendDataModel,omitempty"` +} + +// UpdateComponents updates a surface with a new set of components. +type UpdateComponents struct { + SurfaceID string `json:"surfaceId"` + Components []Component `json:"components"` +} + +// UpdateDataModel updates the data model for a surface. +type UpdateDataModel struct { + SurfaceID string `json:"surfaceId"` + Path string `json:"path,omitempty"` + Value any `json:"value,omitempty"` +} + +// DeleteSurface signals the client to delete a surface. +type DeleteSurface struct { + SurfaceID string `json:"surfaceId"` +} + +// ClientMessage is a message sent from the renderer to the agent. +// Exactly one of Action or Error is non-nil. +type ClientMessage struct { + Version string `json:"version"` + Action *ActionEvent `json:"action,omitempty"` + Error *ClientError `json:"error,omitempty"` +} + +// VersionString returns the A2UI protocol version carried by m. +func (m ClientMessage) VersionString() string { return m.Version } + +// ActionEvent reports a user-initiated action from a component. +type ActionEvent struct { + Name string `json:"name"` + SurfaceID string `json:"surfaceId"` + SourceComponentID string `json:"sourceComponentId"` + Timestamp string `json:"timestamp"` + Context map[string]any `json:"context"` +} + +// ClientError reports a client-side error. +type ClientError struct { + Code string `json:"code"` + SurfaceID string `json:"surfaceId"` + Message string `json:"message"` + Path string `json:"path,omitempty"` +} diff --git a/agent_sdks/go/a2ui/v091/message_json.go b/agent_sdks/go/a2ui/v091/message_json.go new file mode 100644 index 0000000000..a2cf650cc1 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/message_json.go @@ -0,0 +1,76 @@ +package v091 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON implements json.Marshaler for ServerMessage. +func (m ServerMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ServerMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ServerMessage. +func (m *ServerMessage) UnmarshalJSON(data []byte) error { + type alias ServerMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal server message: %w", err) + } + msg := ServerMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ServerMessage) validate() error { + switch countSet(m.CreateSurface != nil, m.UpdateComponents != nil, m.UpdateDataModel != nil, m.DeleteSurface != nil) { + case 1: + return nil + case 0: + return fmt.Errorf("a2ui: server message has no payload set") + default: + return fmt.Errorf("a2ui: server message has multiple payloads set") + } +} + +// MarshalJSON implements json.Marshaler for ClientMessage. +func (m ClientMessage) MarshalJSON() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + type alias ClientMessage + return json.Marshal(alias(m)) +} + +// UnmarshalJSON implements json.Unmarshaler for ClientMessage. +func (m *ClientMessage) UnmarshalJSON(data []byte) error { + type alias ClientMessage + var am alias + if err := json.Unmarshal(data, &am); err != nil { + return fmt.Errorf("a2ui: unmarshal client message: %w", err) + } + msg := ClientMessage(am) + if err := msg.validate(); err != nil { + return err + } + *m = msg + return nil +} + +func (m ClientMessage) validate() error { + switch countSet(m.Action != nil, m.Error != nil) { + case 1: + return nil + case 0: + return fmt.Errorf("a2ui: client message has no payload set") + default: + return fmt.Errorf("a2ui: client message has multiple payloads set") + } +} diff --git a/agent_sdks/go/a2ui/v091/message_test.go b/agent_sdks/go/a2ui/v091/message_test.go new file mode 100644 index 0000000000..1b9d784b78 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/message_test.go @@ -0,0 +1,320 @@ +package v091 + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +func TestFlightStatusMessages(t *testing.T) { + data, err := os.ReadFile(basicExamplesDir + "/01_flight-status.json") + if err != nil { + t.Fatal(err) + } + + var example struct { + Name string `json:"name"` + Description string `json:"description"` + Messages []json.RawMessage `json:"messages"` + } + if err := json.Unmarshal(data, &example); err != nil { + t.Fatal(err) + } + if len(example.Messages) != 3 { + t.Fatalf("got %d messages, want 3", len(example.Messages)) + } + + tests := []struct { + name string + index int + wantField string + }{ + {"CreateSurface", 0, "createSurface"}, + {"UpdateComponents", 1, "updateComponents"}, + {"UpdateDataModel", 2, "updateDataModel"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var msg ServerMessage + if err := json.Unmarshal(example.Messages[tt.index], &msg); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if msg.Version != Version { + t.Fatalf("version = %q, want %q", msg.Version, Version) + } + switch tt.wantField { + case "createSurface": + if msg.CreateSurface == nil { + t.Fatal("CreateSurface is nil") + } + if msg.CreateSurface.SurfaceID != "gallery-flight-status" { + t.Fatalf("surfaceId = %q", msg.CreateSurface.SurfaceID) + } + case "updateComponents": + if msg.UpdateComponents == nil { + t.Fatal("UpdateComponents is nil") + } + if len(msg.UpdateComponents.Components) == 0 { + t.Fatal("no components") + } + case "updateDataModel": + if msg.UpdateDataModel == nil { + t.Fatal("UpdateDataModel is nil") + } + } + + // Round-trip: marshal and compare JSON equivalence. + jsonEquivalent(t, example.Messages[tt.index], msg) + }) + } +} + +func TestServerMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "create_surface", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{ + SurfaceID: "test-1", + CatalogID: "https://example.com/catalog.json", + }, + }, + }, + { + name: "delete_surface", + msg: ServerMessage{ + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "test-1"}, + }, + }, + { + name: "update_data_model", + msg: ServerMessage{ + Version: Version, + UpdateDataModel: &UpdateDataModel{ + SurfaceID: "test-1", + Path: "/count", + Value: float64(42), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestClientMessageRoundTrip(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "action", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{ + Name: "submit", + SurfaceID: "test-1", + SourceComponentID: "btn-1", + Timestamp: "2025-01-01T00:00:00Z", + Context: map[string]any{"key": "value"}, + }, + }, + }, + { + name: "error", + msg: ClientMessage{ + Version: Version, + Error: &ClientError{ + Code: "INVALID_SURFACE", + SurfaceID: "test-1", + Message: "surface not found", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ClientMessage + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, tt.msg) { + t.Fatalf("round-trip mismatch:\n got: %+v\n want: %+v", got, tt.msg) + } + }) + } +} + +func TestServerMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ServerMessage + }{ + { + name: "none", + msg: ServerMessage{Version: Version}, + }, + { + name: "multiple", + msg: ServerMessage{ + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.9.1"}`, + `{"version":"v0.9.1","createSurface":{"surfaceId":"s1","catalogId":"cat"},"deleteSurface":{"surfaceId":"s1"}}`, + } { + var msg ServerMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestClientMessageRejectsInvalidPayloadCounts(t *testing.T) { + tests := []struct { + name string + msg ClientMessage + }{ + { + name: "none", + msg: ClientMessage{Version: Version}, + }, + { + name: "multiple", + msg: ClientMessage{ + Version: Version, + Action: &ActionEvent{Name: "submit", SurfaceID: "s1", SourceComponentID: "btn", Timestamp: "2025-01-01T00:00:00Z"}, + Error: &ClientError{Code: "ERR", SurfaceID: "s1", Message: "bad"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := json.Marshal(tt.msg); err == nil { + t.Fatal("expected marshal error, got nil") + } + }) + } + for _, raw := range []string{ + `{"version":"v0.9.1"}`, + `{"version":"v0.9.1","action":{"name":"submit","surfaceId":"s1","sourceComponentId":"btn","timestamp":"2025-01-01T00:00:00Z"},"error":{"code":"ERR","surfaceId":"s1","message":"bad"}}`, + } { + var msg ClientMessage + if err := json.Unmarshal([]byte(raw), &msg); err == nil { + t.Fatalf("expected unmarshal error for %s", raw) + } + } +} + +func TestServerMessageListWrapper(t *testing.T) { + wrapper := ServerMessageListWrapper{ + Messages: []ServerMessage{ + { + Version: Version, + CreateSurface: &CreateSurface{SurfaceID: "s1", CatalogID: "cat"}, + }, + { + Version: Version, + DeleteSurface: &DeleteSurface{SurfaceID: "s1"}, + }, + }, + } + data, err := json.Marshal(wrapper) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got ServerMessageListWrapper + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !reflect.DeepEqual(got, wrapper) { + t.Fatalf("round-trip mismatch\n got: %+v\n want: %+v", got, wrapper) + } +} + +func TestClientMessageListWrapperEmpty(t *testing.T) { + data := []byte(`{"messages":[]}`) + var w ClientMessageListWrapper + if err := json.Unmarshal(data, &w); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(w.Messages) != 0 { + t.Fatalf("got %d messages, want 0", len(w.Messages)) + } +} + +// jsonEquivalent marshals v and checks that the result is semantically +// equivalent to the original JSON. Empty maps/objects and missing fields +// are treated as equivalent (omitempty normalization). +func jsonEquivalent(t *testing.T, original json.RawMessage, v any) { + t.Helper() + remarshaled, err := json.Marshal(v) + if err != nil { + t.Fatalf("re-marshal: %v", err) + } + var got, want any + if err := json.Unmarshal(remarshaled, &got); err != nil { + t.Fatalf("unmarshal re-marshaled: %v", err) + } + if err := json.Unmarshal(original, &want); err != nil { + t.Fatalf("unmarshal original: %v", err) + } + normalizeJSON(got) + normalizeJSON(want) + if !reflect.DeepEqual(got, want) { + t.Fatalf("JSON not equivalent:\n got: %s\n want: %s", remarshaled, original) + } +} + +// normalizeJSON removes empty maps and nil values in-place so that +// omitempty differences don't cause false mismatches. +func normalizeJSON(v any) { + switch v := v.(type) { + case map[string]any: + for k, val := range v { + normalizeJSON(val) + // Remove keys whose value is an empty map (matches omitempty behavior). + if m, ok := val.(map[string]any); ok && len(m) == 0 { + delete(v, k) + } + } + case []any: + for _, elem := range v { + normalizeJSON(elem) + } + } +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/01_flight-status.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/01_flight-status.json new file mode 100644 index 0000000000..aef954225c --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/01_flight-status.json @@ -0,0 +1,201 @@ +{ + "name": "Flight Status", + "description": "Example of flight status demonstrating date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-flight-status", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-flight-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "route-row", "divider", "times-row"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-left", "date"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "header-left", + "component": "Row", + "children": ["flight-indicator", "flight-number"], + "align": "center" + }, + { + "id": "flight-indicator", + "component": "Icon", + "name": "send" + }, + { + "id": "flight-number", + "component": "Text", + "text": { + "path": "/flightNumber" + }, + "variant": "h3" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "E, MMM d" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "route-row", + "component": "Row", + "children": ["origin", "arrow", "destination"], + "align": "center" + }, + { + "id": "origin", + "component": "Text", + "text": { + "path": "/origin" + }, + "variant": "h2" + }, + { + "id": "arrow", + "component": "Text", + "text": "\u2192", + "variant": "h2" + }, + { + "id": "destination", + "component": "Text", + "text": { + "path": "/destination" + }, + "variant": "h2" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "times-row", + "component": "Row", + "children": ["departure-col", "status-col", "arrival-col"], + "justify": "spaceBetween" + }, + { + "id": "departure-col", + "component": "Column", + "children": ["departure-label", "departure-time"], + "align": "start" + }, + { + "id": "departure-label", + "component": "Text", + "text": "Departs", + "variant": "caption" + }, + { + "id": "departure-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/departureTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "status-col", + "component": "Column", + "children": ["status-label", "status-value"], + "align": "center" + }, + { + "id": "status-label", + "component": "Text", + "text": "Status", + "variant": "caption" + }, + { + "id": "status-value", + "component": "Text", + "text": { + "path": "/status" + }, + "variant": "body" + }, + { + "id": "arrival-col", + "component": "Column", + "children": ["arrival-label", "arrival-time"], + "align": "end" + }, + { + "id": "arrival-label", + "component": "Text", + "text": "Arrives", + "variant": "caption" + }, + { + "id": "arrival-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/arrivalTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-flight-status", + "value": { + "flightNumber": "OS 87", + "date": "2025-12-15", + "origin": "Vienna", + "destination": "New York", + "departureTime": "2025-12-15T10:15:00Z", + "status": "On Time", + "arrivalTime": "2025-12-15T14:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/02_email-compose.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/02_email-compose.json new file mode 100644 index 0000000000..4eb081bc2e --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/02_email-compose.json @@ -0,0 +1,185 @@ +{ + "name": "Email Compose", + "description": "Example of email compose", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-email-compose", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-email-compose", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["from-row", "to-row", "subject-row", "divider", "message", "actions"] + }, + { + "id": "from-row", + "component": "Row", + "children": ["from-label", "from-value"], + "align": "center" + }, + { + "id": "from-label", + "component": "Text", + "text": "FROM", + "variant": "caption" + }, + { + "id": "from-value", + "component": "Text", + "text": { + "path": "/from" + }, + "variant": "body" + }, + { + "id": "to-row", + "component": "Row", + "children": ["to-label", "to-value"], + "align": "center" + }, + { + "id": "to-label", + "component": "Text", + "text": "TO", + "variant": "caption" + }, + { + "id": "to-value", + "component": "Text", + "text": { + "path": "/to" + }, + "variant": "body" + }, + { + "id": "subject-row", + "component": "Row", + "children": ["subject-label", "subject-value"], + "align": "center" + }, + { + "id": "subject-label", + "component": "Text", + "text": "SUBJECT", + "variant": "caption" + }, + { + "id": "subject-value", + "component": "Text", + "text": { + "path": "/subject" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "message", + "component": "Column", + "children": ["greeting", "body-text", "closing", "signature"] + }, + { + "id": "greeting", + "component": "Text", + "text": { + "path": "/greeting" + }, + "variant": "body" + }, + { + "id": "body-text", + "component": "Text", + "text": { + "path": "/body" + }, + "variant": "body" + }, + { + "id": "closing", + "component": "Text", + "text": { + "path": "/closing" + }, + "variant": "body" + }, + { + "id": "signature", + "component": "Text", + "text": { + "path": "/signature" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["send-btn", "discard-btn"] + }, + { + "id": "send-btn-text", + "component": "Text", + "text": "Send email" + }, + { + "id": "send-btn", + "component": "Button", + "child": "send-btn-text", + "action": { + "event": { + "name": "send", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-email-compose", + "value": { + "from": "alex@acme.com", + "to": "jordan@acme.com", + "subject": "Q4 Revenue Forecast", + "greeting": "Hi Jordan,", + "body": "Following up on our call. Please review the attached Q4 forecast and let me know if you have questions before the board meeting.", + "closing": "Best,", + "signature": "Alex" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/03_calendar-day.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/03_calendar-day.json new file mode 100644 index 0000000000..301c3d1dd4 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/03_calendar-day.json @@ -0,0 +1,166 @@ +{ + "name": "Calendar Day", + "description": "Example of calendar day demonstrating dynamic templating, relative paths, and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-calendar-day", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-calendar-day", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "divider", "actions"] + }, + { + "id": "header-row", + "component": "Row", + "children": ["date-col", "events-col"] + }, + { + "id": "date-col", + "component": "Column", + "children": ["day-name", "day-number"], + "align": "start" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-number", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "d" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "events-col", + "component": "Column", + "children": { + "path": "/events", + "componentId": "event-template" + } + }, + { + "id": "event-template", + "component": "Column", + "children": ["event-title", "event-time"] + }, + { + "id": "event-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "event-time", + "component": "Text", + "text": { + "path": "time" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-btn", "discard-btn"] + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to calendar" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-calendar-day", + "value": { + "date": "2025-12-28", + "events": [ + { + "title": "Lunch", + "time": "12:00 - 12:45 PM" + }, + { + "title": "Q1 roadmap review", + "time": "1:00 - 2:00 PM" + }, + { + "title": "Team standup", + "time": "3:30 - 4:00 PM" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/04_weather-current.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/04_weather-current.json new file mode 100644 index 0000000000..a2e1ebd5f7 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/04_weather-current.json @@ -0,0 +1,168 @@ +{ + "name": "Weather Current", + "description": "Example of weather current demonstrating templating and string formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-weather-current", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-weather-current", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["temp-row", "location", "description", "forecast-row"], + "align": "center" + }, + { + "id": "temp-row", + "component": "Row", + "children": ["temp-high", "temp-low"], + "align": "start" + }, + { + "id": "temp-high", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempHigh}°" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "temp-low", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempLow}°" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "location", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "caption" + }, + { + "id": "forecast-row", + "component": "Row", + "children": { + "path": "/forecast", + "componentId": "forecast-day-template" + }, + "justify": "spaceAround" + }, + { + "id": "forecast-day-template", + "component": "Column", + "children": ["day-name", "day-icon", "day-temp"], + "align": "center" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "date" + }, + "format": "E" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-icon", + "component": "Text", + "text": { + "path": "icon" + }, + "variant": "h3" + }, + { + "id": "day-temp", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${temp}°" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-weather-current", + "value": { + "tempHigh": 72, + "tempLow": 58, + "location": "Austin, TX", + "description": "Clear skies with light breeze", + "forecast": [ + { + "date": "2025-12-16", + "icon": "☀️", + "temp": 74 + }, + { + "date": "2025-12-17", + "icon": "☀️", + "temp": 76 + }, + { + "date": "2025-12-18", + "icon": "⛅", + "temp": 71 + }, + { + "date": "2025-12-19", + "icon": "☀️", + "temp": 73 + }, + { + "date": "2025-12-20", + "icon": "☀️", + "temp": 75 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/05_product-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/05_product-card.json new file mode 100644 index 0000000000..ee6634062b --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/05_product-card.json @@ -0,0 +1,151 @@ +{ + "name": "Product Card", + "description": "Example of product card demonstrating currency formatting and pluralization.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-product-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-product-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["image", "details"] + }, + { + "id": "image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "details", + "component": "Column", + "children": ["name", "rating-row", "price-row", "actions"] + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["stars", "reviews"], + "align": "center" + }, + { + "id": "stars", + "component": "Text", + "text": { + "path": "/stars" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "price-row", + "component": "Row", + "children": ["price", "original-price"], + "align": "start" + }, + { + "id": "price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "original-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/originalPrice" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-cart-btn"] + }, + { + "id": "add-cart-btn-text", + "component": "Text", + "text": "Add to Cart" + }, + { + "id": "add-cart-btn", + "component": "Button", + "child": "add-cart-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "addToCart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-product-card", + "value": { + "imageUrl": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&h=200&fit=crop", + "name": "Wireless Headphones Pro", + "stars": "★★★★★", + "reviewCount": 2847, + "price": 199.99, + "originalPrice": 249.99 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/06_music-player.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/06_music-player.json new file mode 100644 index 0000000000..249e17cce4 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/06_music-player.json @@ -0,0 +1,165 @@ +{ + "name": "Music Player", + "description": "Example of music player", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-music-player", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-music-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["album-art", "track-info", "progress", "time-row", "controls"], + "align": "center" + }, + { + "id": "album-art", + "component": "Image", + "url": { + "path": "/albumArt" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["song-title", "artist"], + "align": "center" + }, + { + "id": "song-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "artist", + "component": "Text", + "text": { + "path": "/artist" + }, + "variant": "caption" + }, + { + "id": "progress", + "component": "Slider", + "value": { + "path": "/progress" + }, + "max": 1 + }, + { + "id": "time-row", + "component": "Row", + "children": ["current-time", "total-time"], + "justify": "spaceBetween" + }, + { + "id": "current-time", + "component": "Text", + "text": { + "path": "/currentTime" + }, + "variant": "caption" + }, + { + "id": "total-time", + "component": "Text", + "text": { + "path": "/totalTime" + }, + "variant": "caption" + }, + { + "id": "controls", + "component": "Row", + "children": ["prev-btn", "play-btn", "next-btn"], + "justify": "center" + }, + { + "id": "prev-btn-icon", + "component": "Icon", + "name": "skipPrevious" + }, + { + "id": "prev-btn", + "component": "Button", + "child": "prev-btn-icon", + "action": { + "event": { + "name": "previous", + "context": {} + } + } + }, + { + "id": "play-btn-icon", + "component": "Icon", + "name": { + "path": "/playIcon" + } + }, + { + "id": "play-btn", + "component": "Button", + "child": "play-btn-icon", + "action": { + "event": { + "name": "playPause", + "context": {} + } + } + }, + { + "id": "next-btn-icon", + "component": "Icon", + "name": "skipNext" + }, + { + "id": "next-btn", + "component": "Button", + "child": "next-btn-icon", + "action": { + "event": { + "name": "next", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-music-player", + "value": { + "albumArt": "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=300&h=300&fit=crop", + "title": "Blinding Lights", + "artist": "The Weeknd", + "album": "After Hours", + "progress": 0.45, + "currentTime": "1:48", + "totalTime": "4:22", + "playIcon": "pause" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/07_task-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/07_task-card.json new file mode 100644 index 0000000000..9b6940318b --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/07_task-card.json @@ -0,0 +1,107 @@ +{ + "name": "Task Card", + "description": "Example of task card demonstrating CheckBox and DateTimeInput.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-task-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-task-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["status-checkbox", "content", "priority"], + "align": "start" + }, + { + "id": "status-checkbox", + "component": "CheckBox", + "label": "", + "value": { + "path": "/completed" + } + }, + { + "id": "content", + "component": "Column", + "children": ["title", "description", "meta-row"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["due-date-input", "project"], + "align": "center" + }, + { + "id": "due-date-input", + "component": "DateTimeInput", + "label": "Due", + "value": { + "path": "/dueDate" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "project", + "component": "Text", + "text": { + "path": "/project" + }, + "variant": "caption" + }, + { + "id": "priority", + "component": "Icon", + "name": { + "path": "/priorityIcon" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-task-card", + "value": { + "completed": false, + "title": "Review pull request", + "description": "Review and approve the authentication module changes.", + "dueDate": "2025-12-15T17:00:00Z", + "project": "Backend", + "priorityIcon": "priority_high" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/08_user-profile.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/08_user-profile.json new file mode 100644 index 0000000000..ecec1ad7ec --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/08_user-profile.json @@ -0,0 +1,190 @@ +{ + "name": "User Profile", + "description": "Example of user profile demonstrating number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-user-profile", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-user-profile", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "info", "bio", "stats-row", "follow-btn"], + "align": "center" + }, + { + "id": "header", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "info", + "component": "Column", + "children": ["name", "username"], + "align": "center" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "username", + "component": "Text", + "text": { + "path": "/username" + }, + "variant": "caption" + }, + { + "id": "bio", + "component": "Text", + "text": { + "path": "/bio" + }, + "variant": "body" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["followers-col", "following-col", "posts-col"], + "justify": "spaceAround" + }, + { + "id": "followers-col", + "component": "Column", + "children": ["followers-count", "followers-label"], + "align": "center" + }, + { + "id": "followers-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/followers" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "followers-label", + "component": "Text", + "text": "Followers", + "variant": "caption" + }, + { + "id": "following-col", + "component": "Column", + "children": ["following-count", "following-label"], + "align": "center" + }, + { + "id": "following-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/following" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "following-label", + "component": "Text", + "text": "Following", + "variant": "caption" + }, + { + "id": "posts-col", + "component": "Column", + "children": ["posts-count", "posts-label"], + "align": "center" + }, + { + "id": "posts-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/posts" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "posts-label", + "component": "Text", + "text": "Posts", + "variant": "caption" + }, + { + "id": "follow-btn-text", + "component": "Text", + "text": { + "path": "/followText" + } + }, + { + "id": "follow-btn", + "component": "Button", + "child": "follow-btn-text", + "action": { + "event": { + "name": "follow", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-user-profile", + "value": { + "avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop", + "name": "Sarah Chen", + "username": "@sarahchen", + "bio": "Product Designer at Tech Co. Creating delightful experiences.", + "followers": 12400, + "following": 892, + "posts": 347, + "followText": "Follow" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/09_login-form.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/09_login-form.json new file mode 100644 index 0000000000..8c542e6791 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/09_login-form.json @@ -0,0 +1,214 @@ +{ + "name": "Login Form with Validation", + "description": "Example of login form demonstrating validation checks and logic.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-login-form", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-login-form", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header", + "email-field", + "password-field", + "login-btn", + "divider", + "signup-text" + ] + }, + { + "id": "header", + "component": "Column", + "children": ["title", "subtitle"], + "align": "center" + }, + { + "id": "title", + "component": "Text", + "text": "Welcome back", + "variant": "h2" + }, + { + "id": "subtitle", + "component": "Text", + "text": "Sign in to your account", + "variant": "caption" + }, + { + "id": "email-field", + "component": "TextField", + "value": { + "path": "/email" + }, + "label": "Email", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Email is required" + }, + { + "condition": { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Please enter a valid email address" + } + ] + }, + { + "id": "password-field", + "component": "TextField", + "value": { + "path": "/password" + }, + "label": "Password", + "variant": "obscured", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/password" + } + } + }, + "message": "Password is required" + }, + { + "condition": { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + }, + "message": "Password must be at least 8 characters long" + } + ] + }, + { + "id": "login-btn-text", + "component": "Text", + "text": "Sign in" + }, + { + "id": "login-btn", + "component": "Button", + "child": "login-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + } + ] + } + }, + "message": "Please fix errors before signing in" + } + ], + "action": { + "event": { + "name": "login", + "context": { + "email": { + "path": "/email" + } + } + } + } + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "signup-text", + "component": "Row", + "children": ["no-account", "signup-link"], + "justify": "center" + }, + { + "id": "no-account", + "component": "Text", + "text": "Don't have an account?", + "variant": "caption" + }, + { + "id": "signup-link-text", + "component": "Text", + "text": "Sign up" + }, + { + "id": "signup-link", + "component": "Button", + "child": "signup-link-text", + "action": { + "event": { + "name": "signup", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-login-form", + "value": { + "email": "", + "password": "" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/10_notification-permission.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/10_notification-permission.json new file mode 100644 index 0000000000..956cd8fe04 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/10_notification-permission.json @@ -0,0 +1,105 @@ +{ + "name": "Notification Permission", + "description": "Example of notification permission", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-notification-permission", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-notification-permission", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["icon", "title", "description", "actions"], + "align": "center" + }, + { + "id": "icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["yes-btn", "no-btn"], + "justify": "center" + }, + { + "id": "yes-btn-text", + "component": "Text", + "text": "Yes" + }, + { + "id": "yes-btn", + "component": "Button", + "child": "yes-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "no-btn-text", + "component": "Text", + "text": "No" + }, + { + "id": "no-btn", + "component": "Button", + "child": "no-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-notification-permission", + "value": { + "icon": "check", + "title": "Enable notification", + "description": "Get alerts for order status changes" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/11_purchase-complete.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/11_purchase-complete.json new file mode 100644 index 0000000000..9d27d4e204 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/11_purchase-complete.json @@ -0,0 +1,169 @@ +{ + "name": "Purchase Complete", + "description": "Example of purchase complete demonstrating currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-purchase-complete", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-purchase-complete", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "success-icon", + "title", + "product-row", + "divider", + "details-col", + "view-btn" + ], + "align": "center" + }, + { + "id": "success-icon", + "component": "Icon", + "name": "check" + }, + { + "id": "title", + "component": "Text", + "text": "Purchase Complete", + "variant": "h2" + }, + { + "id": "product-row", + "component": "Row", + "children": ["product-image", "product-info"], + "align": "center" + }, + { + "id": "product-image", + "component": "Image", + "url": { + "path": "/productImage" + }, + "fit": "cover" + }, + { + "id": "product-info", + "component": "Column", + "children": ["product-name", "product-price"] + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h4" + }, + { + "id": "product-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "details-col", + "component": "Column", + "children": ["delivery-row", "seller-row"] + }, + { + "id": "delivery-row", + "component": "Row", + "children": ["delivery-icon", "delivery-text"], + "align": "center" + }, + { + "id": "delivery-icon", + "component": "Icon", + "name": "arrowForward" + }, + { + "id": "delivery-text", + "component": "Text", + "text": { + "path": "/deliveryDate" + }, + "variant": "body" + }, + { + "id": "seller-row", + "component": "Row", + "children": ["seller-label", "seller-name"] + }, + { + "id": "seller-label", + "component": "Text", + "text": "Sold by:", + "variant": "caption" + }, + { + "id": "seller-name", + "component": "Text", + "text": { + "path": "/seller" + }, + "variant": "body" + }, + { + "id": "view-btn-text", + "component": "Text", + "text": "View Order Details" + }, + { + "id": "view-btn", + "component": "Button", + "child": "view-btn-text", + "action": { + "event": { + "name": "view_details", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-purchase-complete", + "value": { + "productImage": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&h=100&fit=crop", + "productName": "Wireless Headphones Pro", + "price": 199.99, + "deliveryDate": "Arrives Dec 18 - Dec 20", + "seller": "TechStore Official" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/12_chat-message.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/12_chat-message.json new file mode 100644 index 0000000000..483e11a4a3 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/12_chat-message.json @@ -0,0 +1,144 @@ +{ + "name": "Chat Message", + "description": "Example of chat message demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-chat-message", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-chat-message", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "messages-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["channel-icon", "channel-name"], + "align": "center" + }, + { + "id": "channel-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "channel-name", + "component": "Text", + "text": { + "path": "/channelName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "messages-list", + "component": "Column", + "children": { + "path": "/messages", + "componentId": "message-template" + }, + "align": "start" + }, + { + "id": "message-template", + "component": "Row", + "children": ["msg-avatar", "msg-content"], + "align": "start" + }, + { + "id": "msg-avatar", + "component": "Image", + "url": { + "path": "avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "msg-content", + "component": "Column", + "children": ["msg-header", "msg-text"] + }, + { + "id": "msg-header", + "component": "Row", + "children": ["msg-username", "msg-time"], + "align": "center" + }, + { + "id": "msg-username", + "component": "Text", + "text": { + "path": "username" + }, + "variant": "h4" + }, + { + "id": "msg-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "timestamp" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "msg-text", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-chat-message", + "value": { + "channelName": "project-updates", + "messages": [ + { + "avatar": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop", + "username": "Mike Chen", + "timestamp": "2025-12-15T10:32:00Z", + "text": "Just pushed the new API changes. Ready for review." + }, + { + "avatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=40&h=40&fit=crop", + "username": "Sarah Kim", + "timestamp": "2025-12-15T10:45:00Z", + "text": "Great! I'll take a look after standup." + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/13_coffee-order.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/13_coffee-order.json new file mode 100644 index 0000000000..86ca714ef0 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/13_coffee-order.json @@ -0,0 +1,253 @@ +{ + "name": "Coffee Order", + "description": "Example of coffee order demonstrating templating and currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-coffee-order", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-coffee-order", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "items-list", "divider", "totals", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["coffee-icon", "store-name"], + "align": "center" + }, + { + "id": "coffee-icon", + "component": "Icon", + "name": "favorite" + }, + { + "id": "store-name", + "component": "Text", + "text": { + "path": "/storeName" + }, + "variant": "h3" + }, + { + "id": "items-list", + "component": "Column", + "children": { + "path": "/items", + "componentId": "order-item-template" + } + }, + { + "id": "order-item-template", + "component": "Row", + "children": ["item-details", "item-price"], + "justify": "spaceBetween", + "align": "start" + }, + { + "id": "item-details", + "component": "Column", + "children": ["item-name", "item-size"] + }, + { + "id": "item-name", + "component": "Text", + "text": { + "path": "name" + }, + "variant": "body" + }, + { + "id": "item-size", + "component": "Text", + "text": { + "path": "size" + }, + "variant": "caption" + }, + { + "id": "item-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "totals", + "component": "Column", + "children": ["subtotal-row", "tax-row", "total-row"] + }, + { + "id": "subtotal-row", + "component": "Row", + "children": ["subtotal-label", "subtotal-value"], + "justify": "spaceBetween" + }, + { + "id": "subtotal-label", + "component": "Text", + "text": "Subtotal", + "variant": "caption" + }, + { + "id": "subtotal-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/subtotal" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "tax-row", + "component": "Row", + "children": ["tax-label", "tax-value"], + "justify": "spaceBetween" + }, + { + "id": "tax-label", + "component": "Text", + "text": "Tax", + "variant": "caption" + }, + { + "id": "tax-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/tax" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/total" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h4" + }, + { + "id": "actions", + "component": "Row", + "children": ["purchase-btn", "add-btn"] + }, + { + "id": "purchase-btn-text", + "component": "Text", + "text": "Purchase" + }, + { + "id": "purchase-btn", + "component": "Button", + "child": "purchase-btn-text", + "action": { + "event": { + "name": "purchase", + "context": {} + } + } + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to cart" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add_to_cart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-coffee-order", + "value": { + "storeName": "Sunrise Coffee", + "items": [ + { + "name": "Oat Milk Latte", + "size": "Grande, Extra Shot", + "price": 6.45 + }, + { + "name": "Chocolate Croissant", + "size": "Warmed", + "price": 4.25 + } + ], + "subtotal": 10.7, + "tax": 0.96, + "total": 11.66 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/14_sports-player.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/14_sports-player.json new file mode 100644 index 0000000000..98e48f6a7c --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/14_sports-player.json @@ -0,0 +1,177 @@ +{ + "name": "Sports Player", + "description": "Example of sports player", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-sports-player", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-sports-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["player-image", "player-info", "divider", "stats-row"], + "align": "center" + }, + { + "id": "player-image", + "component": "Image", + "url": { + "path": "/playerImage" + }, + "fit": "cover" + }, + { + "id": "player-info", + "component": "Column", + "children": ["player-name", "player-details"], + "align": "center" + }, + { + "id": "player-name", + "component": "Text", + "text": { + "path": "/playerName" + }, + "variant": "h2" + }, + { + "id": "player-details", + "component": "Row", + "children": ["player-number", "player-team"], + "align": "center" + }, + { + "id": "player-number", + "component": "Text", + "text": { + "path": "/number" + }, + "variant": "h3" + }, + { + "id": "player-team", + "component": "Text", + "text": { + "path": "/team" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["stat1", "stat2", "stat3"], + "justify": "spaceAround" + }, + { + "id": "stat1", + "component": "Column", + "children": ["stat1-value", "stat1-label"], + "align": "center" + }, + { + "id": "stat1-value", + "component": "Text", + "text": { + "path": "/stat1/value" + }, + "variant": "h3" + }, + { + "id": "stat1-label", + "component": "Text", + "text": { + "path": "/stat1/label" + }, + "variant": "caption" + }, + { + "id": "stat2", + "component": "Column", + "children": ["stat2-value", "stat2-label"], + "align": "center" + }, + { + "id": "stat2-value", + "component": "Text", + "text": { + "path": "/stat2/value" + }, + "variant": "h3" + }, + { + "id": "stat2-label", + "component": "Text", + "text": { + "path": "/stat2/label" + }, + "variant": "caption" + }, + { + "id": "stat3", + "component": "Column", + "children": ["stat3-value", "stat3-label"], + "align": "center" + }, + { + "id": "stat3-value", + "component": "Text", + "text": { + "path": "/stat3/value" + }, + "variant": "h3" + }, + { + "id": "stat3-label", + "component": "Text", + "text": { + "path": "/stat3/label" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-sports-player", + "value": { + "playerImage": "https://images.unsplash.com/photo-1546519638-68e109498ffc?w=200&h=200&fit=crop", + "playerName": "Marcus Johnson", + "number": "#23", + "team": "LA Lakers", + "stat1": { + "value": "28.4", + "label": "PPG" + }, + "stat2": { + "value": "7.2", + "label": "RPG" + }, + "stat3": { + "value": "6.8", + "label": "APG" + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/15_account-balance.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/15_account-balance.json new file mode 100644 index 0000000000..96bdd62934 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/15_account-balance.json @@ -0,0 +1,126 @@ +{ + "name": "Account Balance", + "description": "Example of account balance demonstrating currency formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-account-balance", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-account-balance", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "balance", "updated", "divider", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["account-icon", "account-name"], + "align": "center" + }, + { + "id": "account-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "account-name", + "component": "Text", + "text": { + "path": "/accountName" + }, + "variant": "h4" + }, + { + "id": "balance", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/balance" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "updated", + "component": "Text", + "text": { + "path": "/lastUpdated" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["transfer-btn", "pay-btn"] + }, + { + "id": "transfer-btn-text", + "component": "Text", + "text": "Transfer" + }, + { + "id": "transfer-btn", + "component": "Button", + "child": "transfer-btn-text", + "action": { + "event": { + "name": "transfer", + "context": {} + } + } + }, + { + "id": "pay-btn-text", + "component": "Text", + "text": "Pay Bill" + }, + { + "id": "pay-btn", + "component": "Button", + "child": "pay-btn-text", + "action": { + "event": { + "name": "pay_bill", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-account-balance", + "value": { + "accountName": "Primary Checking", + "balance": 12458.32, + "lastUpdated": "Updated just now" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/16_workout-summary.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/16_workout-summary.json new file mode 100644 index 0000000000..6b43b183ce --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/16_workout-summary.json @@ -0,0 +1,160 @@ +{ + "name": "Workout Summary", + "description": "Example of workout summary demonstrating number and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-workout-summary", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-workout-summary", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "metrics-row", "date"] + }, + { + "id": "header", + "component": "Row", + "children": ["workout-icon", "title"], + "align": "center" + }, + { + "id": "workout-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": "Workout Complete", + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "metrics-row", + "component": "Row", + "children": ["duration-col", "calories-col", "distance-col"], + "justify": "spaceAround" + }, + { + "id": "duration-col", + "component": "Column", + "children": ["duration-value", "duration-label"], + "align": "center" + }, + { + "id": "duration-value", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "h3" + }, + { + "id": "duration-label", + "component": "Text", + "text": "Duration", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} km" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE, MMM d 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-workout-summary", + "value": { + "icon": "directions_run", + "workoutType": "Morning Run", + "duration": "32:15", + "calories": 385, + "distance": 5.2, + "date": "2025-12-15T07:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/17_event-detail.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/17_event-detail.json new file mode 100644 index 0000000000..27264335c4 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/17_event-detail.json @@ -0,0 +1,144 @@ +{ + "name": "Event Detail", + "description": "Example of event detail demonstrating date and string formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-event-detail", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-event-detail", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title", "time-row", "location-row", "description", "divider", "actions"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h2" + }, + { + "id": "time-row", + "component": "Row", + "children": ["time-icon", "time-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "time-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatDate(value: ${/start}, format: 'E, MMM d')} • ${formatDate(value: ${/start}, format: 'h:mm a')} - ${formatDate(value: ${/end}, format: 'h:mm a')}" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["accept-btn", "decline-btn"] + }, + { + "id": "accept-btn-text", + "component": "Text", + "text": "Accept" + }, + { + "id": "accept-btn", + "component": "Button", + "child": "accept-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "decline-btn-text", + "component": "Text", + "text": "Decline" + }, + { + "id": "decline-btn", + "component": "Button", + "child": "decline-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-event-detail", + "value": { + "title": "Product Launch Meeting", + "start": "2025-12-19T14:00:00Z", + "end": "2025-12-19T15:30:00Z", + "location": "Conference Room A, Building 2", + "description": "Review final product specs and marketing materials before the Q1 launch." + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/18_track-list.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/18_track-list.json new file mode 100644 index 0000000000..2db21c4ade --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/18_track-list.json @@ -0,0 +1,152 @@ +{ + "name": "Track List", + "description": "Example of track list demonstrating templating, relative paths, and number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-track-list", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-track-list", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "tracks-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["playlist-icon", "playlist-name"], + "align": "center" + }, + { + "id": "playlist-icon", + "component": "Icon", + "name": "play" + }, + { + "id": "playlist-name", + "component": "Text", + "text": { + "path": "/playlistName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "tracks-list", + "component": "Column", + "children": { + "path": "/tracks", + "componentId": "track-item-template" + } + }, + { + "id": "track-item-template", + "component": "Row", + "children": ["track-num", "track-art", "track-info", "track-duration"], + "align": "center" + }, + { + "id": "track-num", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "number" + } + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "track-art", + "component": "Image", + "url": { + "path": "art" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["track-title", "track-artist"] + }, + { + "id": "track-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "track-artist", + "component": "Text", + "text": { + "path": "artist" + }, + "variant": "caption" + }, + { + "id": "track-duration", + "component": "Text", + "text": { + "path": "duration" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-track-list", + "value": { + "playlistName": "Focus Flow", + "tracks": [ + { + "number": 1, + "art": "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=50&h=50&fit=crop", + "title": "Weightless", + "artist": "Marconi Union", + "duration": "8:09" + }, + { + "number": 2, + "art": "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=50&h=50&fit=crop", + "title": "Clair de Lune", + "artist": "Debussy", + "duration": "5:12" + }, + { + "number": 3, + "art": "https://images.unsplash.com/photo-1507838153414-b4b713384a76?w=50&h=50&fit=crop", + "title": "Ambient Light", + "artist": "Brian Eno", + "duration": "6:45" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/19_software-purchase.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/19_software-purchase.json new file mode 100644 index 0000000000..ee879eed36 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/19_software-purchase.json @@ -0,0 +1,194 @@ +{ + "name": "Software Purchase", + "description": "Example of software purchase demonstrating currency formatting and ChoicePicker.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-software-purchase", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-software-purchase", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "title", + "product-name", + "divider1", + "options", + "divider2", + "total-row", + "actions" + ] + }, + { + "id": "title", + "component": "Text", + "text": "Purchase License", + "variant": "h3" + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h2" + }, + { + "id": "divider1", + "component": "Divider" + }, + { + "id": "options", + "component": "Column", + "children": ["seats-row", "period-row"] + }, + { + "id": "seats-row", + "component": "Row", + "children": ["seats-label", "seats-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "seats-label", + "component": "Text", + "text": "Number of seats", + "variant": "body" + }, + { + "id": "seats-value", + "component": "Text", + "text": { + "path": "/seats" + }, + "variant": "h4" + }, + { + "id": "period-row", + "component": "Row", + "children": ["period-label", "period-picker"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "period-label", + "component": "Text", + "text": "Billing period", + "variant": "body" + }, + { + "id": "period-picker", + "component": "ChoicePicker", + "options": [ + { + "label": "Annual", + "value": "annual" + }, + { + "label": "Monthly", + "value": "monthly" + } + ], + "value": { + "path": "/billingPeriod" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "divider2", + "component": "Divider" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatCurrency(value: ${/total}, currency: 'USD')}/year" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "actions", + "component": "Row", + "children": ["confirm-btn", "cancel-btn"] + }, + { + "id": "confirm-btn-text", + "component": "Text", + "text": "Confirm Purchase" + }, + { + "id": "confirm-btn", + "component": "Button", + "child": "confirm-btn-text", + "action": { + "event": { + "name": "confirm", + "context": {} + } + } + }, + { + "id": "cancel-btn-text", + "component": "Text", + "text": "Cancel" + }, + { + "id": "cancel-btn", + "component": "Button", + "child": "cancel-btn-text", + "action": { + "event": { + "name": "cancel", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-software-purchase", + "value": { + "productName": "Design Suite Pro", + "seats": "10 seats", + "billingPeriod": ["annual"], + "total": 1188.0 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/20_restaurant-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/20_restaurant-card.json new file mode 100644 index 0000000000..ee02cbc2bc --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/20_restaurant-card.json @@ -0,0 +1,140 @@ +{ + "name": "Restaurant Card", + "description": "Example of restaurant card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-restaurant-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-restaurant-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["restaurant-image", "content"] + }, + { + "id": "restaurant-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["name-row", "cuisine", "rating-row", "details-row"] + }, + { + "id": "name-row", + "component": "Row", + "children": ["restaurant-name", "price-range"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "restaurant-name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "price-range", + "component": "Text", + "text": { + "path": "/priceRange" + }, + "variant": "body" + }, + { + "id": "cuisine", + "component": "Text", + "text": { + "path": "/cuisine" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "reviews"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "path": "/reviewCount" + }, + "variant": "caption" + }, + { + "id": "details-row", + "component": "Row", + "children": ["distance", "delivery-time"] + }, + { + "id": "distance", + "component": "Text", + "text": { + "path": "/distance" + }, + "variant": "caption" + }, + { + "id": "delivery-time", + "component": "Text", + "text": { + "path": "/deliveryTime" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-restaurant-card", + "value": { + "image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=300&h=150&fit=crop", + "name": "The Italian Kitchen", + "priceRange": "$$$", + "cuisine": "Italian \u2022 Pasta \u2022 Wine Bar", + "rating": "4.8", + "reviewCount": "(2,847 reviews)", + "distance": "0.8 mi", + "deliveryTime": "25-35 min" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/21_shipping-status.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/21_shipping-status.json new file mode 100644 index 0000000000..d9ea2317b4 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/21_shipping-status.json @@ -0,0 +1,137 @@ +{ + "name": "Shipping Status", + "description": "Example of shipping status demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-shipping-status", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-shipping-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "tracking-number", "divider", "steps-list", "eta"] + }, + { + "id": "header", + "component": "Row", + "children": ["package-icon", "title"], + "align": "center" + }, + { + "id": "package-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "title", + "component": "Text", + "text": "Package Status", + "variant": "h3" + }, + { + "id": "tracking-number", + "component": "Text", + "text": { + "path": "/trackingNumber" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "steps-list", + "component": "Column", + "children": { + "path": "/steps", + "componentId": "step-item-template" + } + }, + { + "id": "step-item-template", + "component": "Row", + "children": ["step-icon", "step-text"], + "align": "center" + }, + { + "id": "step-icon", + "component": "Icon", + "name": { + "path": "icon" + } + }, + { + "id": "step-text", + "component": "Text", + "text": { + "path": "label" + }, + "variant": "body" + }, + { + "id": "eta", + "component": "Row", + "children": ["eta-icon", "eta-text"], + "align": "center" + }, + { + "id": "eta-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "eta-text", + "component": "Text", + "text": { + "path": "/eta" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-shipping-status", + "value": { + "trackingNumber": "Tracking: 1Z999AA10123456784", + "steps": [ + { + "icon": "check", + "label": "Order Placed" + }, + { + "icon": "check", + "label": "Shipped" + }, + { + "icon": "send", + "label": "Out for Delivery" + }, + { + "icon": "check", + "label": "Delivered" + } + ], + "eta": "Estimated delivery: Today by 8 PM" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/22_credit-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/22_credit-card.json new file mode 100644 index 0000000000..5405814388 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/22_credit-card.json @@ -0,0 +1,117 @@ +{ + "name": "Credit Card", + "description": "Example of credit card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-credit-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-credit-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["card-type-row", "card-number", "card-details"] + }, + { + "id": "card-type-row", + "component": "Row", + "children": ["card-icon", "card-type"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "card-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "card-type", + "component": "Text", + "text": { + "path": "/cardType" + }, + "variant": "h4" + }, + { + "id": "card-number", + "component": "Text", + "text": { + "path": "/cardNumber" + }, + "variant": "h2" + }, + { + "id": "card-details", + "component": "Row", + "children": ["holder-col", "expiry-col"], + "justify": "spaceBetween" + }, + { + "id": "holder-col", + "component": "Column", + "children": ["holder-label", "holder-name"] + }, + { + "id": "holder-label", + "component": "Text", + "text": "CARD HOLDER", + "variant": "caption" + }, + { + "id": "holder-name", + "component": "Text", + "text": { + "path": "/holderName" + }, + "variant": "body" + }, + { + "id": "expiry-col", + "component": "Column", + "children": ["expiry-label", "expiry-date"], + "align": "end" + }, + { + "id": "expiry-label", + "component": "Text", + "text": "EXPIRES", + "variant": "caption" + }, + { + "id": "expiry-date", + "component": "Text", + "text": { + "path": "/expiryDate" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-credit-card", + "value": { + "cardType": "VISA", + "cardNumber": "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 4242", + "holderName": "SARAH JOHNSON", + "expiryDate": "09/27" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/23_step-counter.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/23_step-counter.json new file mode 100644 index 0000000000..66b2e3e670 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/23_step-counter.json @@ -0,0 +1,149 @@ +{ + "name": "Step Counter", + "description": "Example of step counter demonstrating number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-step-counter", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-step-counter", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "steps-display", "goal-text", "divider", "stats-row"], + "align": "center" + }, + { + "id": "header", + "component": "Row", + "children": ["steps-icon", "title"], + "align": "center" + }, + { + "id": "steps-icon", + "component": "Icon", + "name": "person" + }, + { + "id": "title", + "component": "Text", + "text": "Today's Steps", + "variant": "h4" + }, + { + "id": "steps-display", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/steps" + } + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "goal-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/progress}% of ${formatNumber(value: ${/goal})} goal" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["distance-col", "calories-col"], + "justify": "spaceAround" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} mi" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-step-counter", + "value": { + "steps": 8432, + "goal": 10000, + "progress": 84, + "distance": 3.8, + "calories": 312 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/24_recipe-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/24_recipe-card.json new file mode 100644 index 0000000000..a150f99ecb --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/24_recipe-card.json @@ -0,0 +1,204 @@ +{ + "name": "Recipe Card", + "description": "Example of recipe card demonstrating Tabs and pluralization.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-recipe-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-recipe-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "tabs-container" + }, + { + "id": "tabs-container", + "component": "Tabs", + "tabs": [ + { + "title": "Overview", + "child": "overview-col" + }, + { + "title": "Ingredients", + "child": "ingredients-list" + }, + { + "title": "Instructions", + "child": "instructions-list" + } + ] + }, + { + "id": "overview-col", + "component": "Column", + "children": ["recipe-image", "overview-content"] + }, + { + "id": "recipe-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "overview-content", + "component": "Column", + "children": ["title", "rating-row", "times-row", "servings"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "review-count"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "review-count", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "times-row", + "component": "Row", + "children": ["prep-time", "cook-time"] + }, + { + "id": "prep-time", + "component": "Row", + "children": ["prep-icon", "prep-text"], + "align": "center" + }, + { + "id": "prep-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "prep-text", + "component": "Text", + "text": { + "path": "/prepTime" + }, + "variant": "caption" + }, + { + "id": "cook-time", + "component": "Row", + "children": ["cook-icon", "cook-text"], + "align": "center" + }, + { + "id": "cook-icon", + "component": "Icon", + "name": "warning" + }, + { + "id": "cook-text", + "component": "Text", + "text": { + "path": "/cookTime" + }, + "variant": "caption" + }, + { + "id": "servings", + "component": "Text", + "text": { + "path": "/servings" + }, + "variant": "caption" + }, + { + "id": "ingredients-list", + "component": "Column", + "children": { + "path": "/ingredients", + "componentId": "item-template" + } + }, + { + "id": "instructions-list", + "component": "Column", + "children": { + "path": "/instructions", + "componentId": "item-template" + } + }, + { + "id": "item-template", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-recipe-card", + "value": { + "image": "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=180&fit=crop", + "title": "Mediterranean Quinoa Bowl", + "rating": "4.9", + "reviewCount": 1247, + "prepTime": "15 min prep", + "cookTime": "20 min cook", + "servings": "Serves 4", + "ingredients": [ + {"text": "1 cup quinoa"}, + {"text": "2 cups water"}, + {"text": "1 cucumber, diced"}, + {"text": "1 cup cherry tomatoes, halved"} + ], + "instructions": [ + {"text": "1. Rinse quinoa and bring to a boil in water."}, + {"text": "2. Reduce heat and simmer for 15 minutes."}, + {"text": "3. Fluff with a fork and let cool."}, + {"text": "4. Mix with diced vegetables."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/25_contact-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/25_contact-card.json new file mode 100644 index 0000000000..4dd7155c1a --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/25_contact-card.json @@ -0,0 +1,175 @@ +{ + "name": "Contact Card", + "description": "Example of contact card", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-contact-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["avatar-image", "name", "title", "divider", "contact-info", "actions"], + "align": "center" + }, + { + "id": "avatar-image", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "contact-info", + "component": "Column", + "children": ["phone-row", "email-row", "location-row"] + }, + { + "id": "phone-row", + "component": "Row", + "children": ["phone-icon", "phone-text"], + "align": "center" + }, + { + "id": "phone-icon", + "component": "Icon", + "name": "phone" + }, + { + "id": "phone-text", + "component": "Text", + "text": { + "path": "/phone" + }, + "variant": "body" + }, + { + "id": "email-row", + "component": "Row", + "children": ["email-icon", "email-text"], + "align": "center" + }, + { + "id": "email-icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "email-text", + "component": "Text", + "text": { + "path": "/email" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["call-btn", "message-btn"] + }, + { + "id": "call-btn-text", + "component": "Text", + "text": "Call" + }, + { + "id": "call-btn", + "component": "Button", + "child": "call-btn-text", + "action": { + "event": { + "name": "call", + "context": {} + } + } + }, + { + "id": "message-btn-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-btn", + "component": "Button", + "child": "message-btn-text", + "action": { + "event": { + "name": "message", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-contact-card", + "value": { + "avatar": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop", + "name": "David Park", + "title": "Engineering Manager", + "phone": "+1 (555) 234-5678", + "email": "david.park@company.com", + "location": "San Francisco, CA" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/26_podcast-episode.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/26_podcast-episode.json new file mode 100644 index 0000000000..0e3b2b806e --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/26_podcast-episode.json @@ -0,0 +1,123 @@ +{ + "name": "Podcast Episode", + "description": "Example of podcast episode demonstrating AudioPlayer and date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-podcast-episode", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-podcast-episode", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["artwork", "content"], + "align": "start" + }, + { + "id": "artwork", + "component": "Image", + "url": { + "path": "/artwork" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["show-name", "episode-title", "meta-row", "description", "audio-player"] + }, + { + "id": "show-name", + "component": "Text", + "text": { + "path": "/showName" + }, + "variant": "caption" + }, + { + "id": "episode-title", + "component": "Text", + "text": { + "path": "/episodeTitle" + }, + "variant": "h4" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["duration", "date"] + }, + { + "id": "duration", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "MMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "audio-player", + "component": "AudioPlayer", + "url": { + "path": "/audioUrl" + }, + "description": { + "path": "/episodeTitle" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-podcast-episode", + "value": { + "artwork": "https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=100&h=100&fit=crop", + "showName": "Tech Talk Daily", + "episodeTitle": "The Future of AI in Product Design", + "duration": "45 min", + "date": "2024-12-15", + "description": "How AI is transforming the way we design and build products.", + "audioUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/27_stats-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/27_stats-card.json new file mode 100644 index 0000000000..8a477e6383 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/27_stats-card.json @@ -0,0 +1,106 @@ +{ + "name": "Stats Card", + "description": "Example of stats card demonstrating currency and number formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-stats-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-stats-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "value", "trend-row"] + }, + { + "id": "header", + "component": "Row", + "children": ["metric-icon", "metric-name"], + "align": "center" + }, + { + "id": "metric-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "metric-name", + "component": "Text", + "text": { + "path": "/metricName" + }, + "variant": "caption" + }, + { + "id": "value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/value" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "trend-row", + "component": "Row", + "children": ["trend-icon", "trend-text"], + "align": "center" + }, + { + "id": "trend-icon", + "component": "Icon", + "name": { + "path": "/trendIcon" + } + }, + { + "id": "trend-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "+${/trendPercent}% from last month" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-stats-card", + "value": { + "icon": "trending_up", + "metricName": "Monthly Revenue", + "value": 48294, + "trendIcon": "arrow_upward", + "trendPercent": 12.5 + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/28_countdown-timer.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/28_countdown-timer.json new file mode 100644 index 0000000000..8513e17f36 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/28_countdown-timer.json @@ -0,0 +1,135 @@ +{ + "name": "Countdown Timer", + "description": "Example of countdown timer demonstrating date formatting.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-countdown-timer", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-countdown-timer", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["event-name", "countdown-row", "target-date"], + "align": "center" + }, + { + "id": "event-name", + "component": "Text", + "text": { + "path": "/eventName" + }, + "variant": "h3" + }, + { + "id": "countdown-row", + "component": "Row", + "children": ["days-col", "hours-col", "minutes-col"], + "justify": "spaceAround" + }, + { + "id": "days-col", + "component": "Column", + "children": ["days-value", "days-label"], + "align": "center" + }, + { + "id": "days-value", + "component": "Text", + "text": { + "path": "/days" + }, + "variant": "h1" + }, + { + "id": "days-label", + "component": "Text", + "text": "Days", + "variant": "caption" + }, + { + "id": "hours-col", + "component": "Column", + "children": ["hours-value", "hours-label"], + "align": "center" + }, + { + "id": "hours-value", + "component": "Text", + "text": { + "path": "/hours" + }, + "variant": "h1" + }, + { + "id": "hours-label", + "component": "Text", + "text": "Hours", + "variant": "caption" + }, + { + "id": "minutes-col", + "component": "Column", + "children": ["minutes-value", "minutes-label"], + "align": "center" + }, + { + "id": "minutes-value", + "component": "Text", + "text": { + "path": "/minutes" + }, + "variant": "h1" + }, + { + "id": "minutes-label", + "component": "Text", + "text": "Minutes", + "variant": "caption" + }, + { + "id": "target-date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/targetDate" + }, + "format": "MMMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-countdown-timer", + "value": { + "eventName": "Product Launch", + "days": "14", + "hours": "08", + "minutes": "32", + "targetDate": "2025-01-15" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/29_movie-card.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/29_movie-card.json new file mode 100644 index 0000000000..ab5a25d182 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/29_movie-card.json @@ -0,0 +1,156 @@ +{ + "name": "Movie Card", + "description": "Example of movie card demonstrating Modal and Video components.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-movie-card", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-movie-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["poster", "content", "trailer-modal"] + }, + { + "id": "poster", + "component": "Image", + "url": { + "path": "/poster" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["title-row", "genre", "rating-row", "runtime", "watch-trailer-btn"] + }, + { + "id": "title-row", + "component": "Row", + "children": ["movie-title", "year"], + "align": "start" + }, + { + "id": "movie-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "year", + "component": "Text", + "text": { + "path": "/year" + }, + "variant": "caption" + }, + { + "id": "genre", + "component": "Text", + "text": { + "path": "/genre" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating-value"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating-value", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "runtime", + "component": "Row", + "children": ["time-icon", "runtime-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "runtime-text", + "component": "Text", + "text": { + "path": "/runtime" + }, + "variant": "caption" + }, + { + "id": "watch-trailer-btn-text", + "component": "Text", + "text": "Watch Trailer" + }, + { + "id": "watch-trailer-btn", + "component": "Button", + "child": "watch-trailer-btn-text", + "action": { + "event": { + "name": "open_trailer" + } + } + }, + { + "id": "trailer-modal", + "component": "Modal", + "trigger": "watch-trailer-btn", + "content": "trailer-video" + }, + { + "id": "trailer-video", + "component": "Video", + "url": { + "path": "/trailerUrl" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-movie-card", + "value": { + "poster": "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=200&h=300&fit=crop", + "title": "Interstellar", + "year": "(2014)", + "genre": "Sci-Fi • Adventure • Drama", + "rating": "8.7/10", + "runtime": "2h 49min", + "trailerUrl": "https://www.w3schools.com/html/mov_bbb.mp4" + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/30_live-invitation-builder.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/30_live-invitation-builder.json new file mode 100644 index 0000000000..06fdd741d3 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/30_live-invitation-builder.json @@ -0,0 +1,205 @@ +{ + "name": "Live Invitation Builder", + "description": "Demonstrates reactive two-way binding where editor inputs update a live preview in real-time.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-invitation-builder", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-invitation-builder", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "main-content"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "# Invitation Builder", + "variant": "h1" + }, + { + "id": "main-content", + "component": "Row", + "children": ["editor-col", "preview-col"], + "align": "start" + }, + { + "id": "editor-col", + "component": "Column", + "children": [ + "editor-title", + "event-name-input", + "guest-input", + "date-input", + "location-picker" + ], + "weight": 1, + "align": "stretch" + }, + { + "id": "editor-title", + "component": "Text", + "text": "Customize your invitation", + "variant": "h3" + }, + { + "id": "event-name-input", + "component": "TextField", + "label": "Event Name", + "value": { + "path": "/event/name" + } + }, + { + "id": "guest-input", + "component": "TextField", + "label": "Guest of Honor", + "value": { + "path": "/event/guest" + } + }, + { + "id": "date-input", + "component": "DateTimeInput", + "label": "Event Date & Time", + "value": { + "path": "/event/date" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "location-picker", + "component": "ChoicePicker", + "label": "Location", + "options": [ + {"label": "Grand Ballroom", "value": "ballroom"}, + {"label": "Sunset Terrace", "value": "terrace"}, + {"label": "Garden Pavillion", "value": "garden"} + ], + "value": { + "path": "/event/location" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "preview-col", + "component": "Column", + "children": ["preview-title", "invitation-card"], + "weight": 1, + "align": "center" + }, + { + "id": "preview-title", + "component": "Text", + "text": "Live Preview", + "variant": "caption" + }, + { + "id": "invitation-card", + "component": "Card", + "child": "invitation-content" + }, + { + "id": "invitation-content", + "component": "Column", + "children": [ + "invite-image", + "invite-event-name", + "invite-guest-row", + "invite-date-text", + "invite-location-text" + ], + "align": "center" + }, + { + "id": "invite-image", + "component": "Image", + "url": "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop", + "variant": "mediumFeature" + }, + { + "id": "invite-event-name", + "component": "Text", + "text": { + "path": "/event/name" + }, + "variant": "h2" + }, + { + "id": "invite-guest-row", + "component": "Row", + "children": ["invite-for-text", "invite-guest-name"], + "align": "center" + }, + { + "id": "invite-for-text", + "component": "Text", + "text": "Celebrating", + "variant": "body" + }, + { + "id": "invite-guest-name", + "component": "Text", + "text": { + "path": "/event/guest" + }, + "variant": "h3" + }, + { + "id": "invite-date-text", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/event/date" + }, + "format": "EEEE, MMMM d, yyyy 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "invite-location-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Location: ${/event/location/0}" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-invitation-builder", + "value": { + "event": { + "name": "Summer Gala", + "guest": "Alex Johnson", + "date": "2025-07-15T19:00:00Z", + "location": ["terrace"] + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/31_incremental-dashboard.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/31_incremental-dashboard.json new file mode 100644 index 0000000000..775a2de37e --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/31_incremental-dashboard.json @@ -0,0 +1,128 @@ +{ + "name": "Incremental Dashboard", + "description": "Demonstrates structural evolution of a UI where loading placeholders are incrementally replaced by actual components.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-incremental-dashboard", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "content-grid"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "System Dashboard", + "variant": "h2" + }, + { + "id": "content-grid", + "component": "Row", + "children": ["left-panel", "right-panel"] + }, + { + "id": "left-panel", + "component": "Column", + "children": ["panel-a-loading"], + "weight": 1 + }, + { + "id": "right-panel", + "component": "Column", + "children": ["panel-b-loading"], + "weight": 1 + }, + { + "id": "panel-a-loading", + "component": "Text", + "text": "Loading analytics...", + "variant": "caption" + }, + { + "id": "panel-b-loading", + "component": "Text", + "text": "Loading logs...", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "left-panel", + "component": "Column", + "children": ["analytics-card"], + "weight": 1 + }, + { + "id": "analytics-card", + "component": "Card", + "child": "analytics-text" + }, + { + "id": "analytics-text", + "component": "Text", + "text": "Analytics are ready.", + "variant": "body" + } + ] + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "right-panel", + "component": "Column", + "children": ["logs-list"], + "weight": 1 + }, + { + "id": "logs-list", + "component": "List", + "children": { + "path": "/logs", + "componentId": "log-template" + } + }, + { + "id": "log-template", + "component": "Text", + "text": {"path": "message"}, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-incremental-dashboard", + "value": { + "logs": [ + {"message": "System boot complete."}, + {"message": "All services healthy."}, + {"message": "Waiting for user input."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/32_advanced-form-validator.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/32_advanced-form-validator.json new file mode 100644 index 0000000000..0279bf34be --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/32_advanced-form-validator.json @@ -0,0 +1,166 @@ +{ + "name": "Advanced Form Validator", + "description": "Demonstrates complex validation logic and deeply nested formatting functions.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-advanced-validator", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-advanced-validator", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "welcome-text", + "email-field", + "phone-field", + "zip-field", + "terms-checkbox", + "submit-btn" + ], + "align": "stretch" + }, + { + "id": "welcome-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Hello! Today is ${formatDate(value: ${/now}, format: 'EEEE, MMMM d')}." + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "email-field", + "component": "TextField", + "label": "Email Address", + "value": {"path": "/formData/email"}, + "checks": [ + { + "condition": { + "call": "email", + "args": {"value": {"path": "/formData/email"}} + }, + "message": "Invalid email format" + } + ] + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone Number", + "value": {"path": "/formData/phone"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": { + "value": {"path": "/formData/phone"}, + "pattern": "^\\+?[0-9]{10,15}$" + } + }, + "message": "Invalid phone format" + } + ] + }, + { + "id": "zip-field", + "component": "TextField", + "label": "Zip Code", + "value": {"path": "/formData/zip"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": {"value": {"path": "/formData/zip"}, "pattern": "^[0-9]{5}$"} + }, + "message": "Must be exactly 5 digits" + } + ] + }, + { + "id": "terms-checkbox", + "component": "CheckBox", + "label": "I agree to the terms and conditions", + "value": {"path": "/formData/agree"} + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit Registration" + }, + { + "id": "submit-btn", + "component": "Button", + "child": "submit-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + {"path": "/formData/agree"}, + { + "call": "or", + "args": { + "values": [ + { + "call": "required", + "args": {"value": {"path": "/formData/email"}} + }, + { + "call": "required", + "args": {"value": {"path": "/formData/phone"}} + } + ] + } + }, + {"call": "required", "args": {"value": {"path": "/formData/zip"}}} + ] + } + }, + "message": "You must agree to terms AND provide either Email or Phone, plus a Zip code." + } + ], + "action": { + "event": { + "name": "register", + "context": {"data": {"path": "/formData"}} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-advanced-validator", + "value": { + "now": "2025-12-15T12:00:00Z", + "formData": { + "email": "", + "phone": "", + "zip": "", + "agree": false + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/33_financial-data-grid.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/33_financial-data-grid.json new file mode 100644 index 0000000000..7d19eb8411 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/33_financial-data-grid.json @@ -0,0 +1,171 @@ +{ + "name": "Financial Data Grid", + "description": "Demonstrates complex layout weighting and alignment using Rows and Columns with templates.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-financial-grid", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-financial-grid", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "grid-list"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["col-asset", "col-price", "col-change", "col-market-cap"], + "align": "center" + }, + { + "id": "col-asset", + "component": "Text", + "text": "Asset", + "variant": "caption", + "weight": 2 + }, + { + "id": "col-price", + "component": "Text", + "text": "Price", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-change", + "component": "Text", + "text": "24h Change", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-market-cap", + "component": "Text", + "text": "Market Cap", + "variant": "caption", + "weight": 1.5 + }, + { + "id": "grid-list", + "component": "List", + "children": { + "path": "/assets", + "componentId": "row-template" + } + }, + { + "id": "row-template", + "component": "Row", + "children": ["asset-info", "asset-price", "asset-change", "asset-market-cap"], + "align": "center" + }, + { + "id": "asset-info", + "component": "Row", + "children": ["asset-icon", "asset-name-col"], + "weight": 2, + "align": "center" + }, + { + "id": "asset-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "asset-name-col", + "component": "Column", + "children": ["asset-name", "asset-symbol"] + }, + { + "id": "asset-name", + "component": "Text", + "text": {"path": "name"}, + "variant": "body" + }, + { + "id": "asset-symbol", + "component": "Text", + "text": {"path": "symbol"}, + "variant": "caption" + }, + { + "id": "asset-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "price"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-change", + "component": "Text", + "text": { + "call": "formatString", + "args": {"value": "${change}%"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-market-cap", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "marketCap"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1.5 + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-financial-grid", + "value": { + "assets": [ + { + "name": "Bitcoin", + "symbol": "BTC", + "price": 43500.25, + "change": 1.2, + "marketCap": 850000000000 + }, + { + "name": "Ethereum", + "symbol": "ETH", + "price": 2250.5, + "change": -0.5, + "marketCap": 270000000000 + }, + { + "name": "Solana", + "symbol": "SOL", + "price": 95.8, + "change": 5.4, + "marketCap": 40000000000 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/34_child-list-template.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/34_child-list-template.json new file mode 100644 index 0000000000..328420b9f7 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/34_child-list-template.json @@ -0,0 +1,80 @@ +{ + "name": "ChildList Template Expansion", + "description": "Demonstrates dynamic list generation using ChildList object templates.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-child-list-template", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-child-list-template", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "item-list"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Dynamic Item List", + "variant": "h3" + }, + { + "id": "item-list", + "component": "List", + "children": { + "componentId": "item-row", + "path": "/items" + } + }, + { + "id": "item-row", + "component": "Row", + "children": ["item-name", "qty-label", "item-qty"] + }, + { + "id": "item-name", + "component": "Text", + "text": {"path": "name"} + }, + { + "id": "qty-label", + "component": "Text", + "text": " - Qty: " + }, + { + "id": "item-qty", + "component": "Text", + "text": {"path": "quantity"} + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "gallery-child-list-template", + "value": { + "items": [ + {"name": "Apple", "quantity": 10}, + {"name": "Banana", "quantity": 5}, + {"name": "Cherry", "quantity": 20} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/35_markdown-text.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/35_markdown-text.json new file mode 100644 index 0000000000..a1f0e5fca9 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/35_markdown-text.json @@ -0,0 +1,44 @@ +{ + "name": "Markdown Text Support", + "description": "Demonstrates Markdown rendering in Text component.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "gallery-markdown-text", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "gallery-markdown-text", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "markdown-content"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Markdown Rendering", + "variant": "h3" + }, + { + "id": "markdown-content", + "component": "Text", + "text": "# Heading 1\n\nThis is **bold** text and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link to Google](https://google.com)" + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/36_modal.json b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/36_modal.json new file mode 100644 index 0000000000..f17e3068e8 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/testdata/v0_9_1/catalogs/basic/examples/36_modal.json @@ -0,0 +1,65 @@ +{ + "name": "Modal Sample", + "description": "Example of Modal component showing a trigger and content.", + "messages": [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "modal-sample-surface", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "modal-sample-surface", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["title", "modal-comp"] + }, + { + "id": "title", + "component": "Text", + "text": "Modal Component Sample", + "variant": "h2" + }, + { + "id": "modal-comp", + "component": "Modal", + "trigger": "open-btn", + "content": "modal-content" + }, + { + "id": "open-btn-text", + "component": "Text", + "text": "Open Modal" + }, + { + "id": "open-btn", + "component": "Button", + "child": "open-btn-text", + "action": { + "event": { + "name": "openModalEvent", + "context": {} + } + } + }, + { + "id": "modal-content", + "component": "Column", + "children": ["modal-text"] + }, + { + "id": "modal-text", + "component": "Text", + "text": "This is the content inside the modal." + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2ui/v091/zz_component.go b/agent_sdks/go/a2ui/v091/zz_component.go new file mode 100644 index 0000000000..0d27abb779 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_component.go @@ -0,0 +1,136 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +// TabDef defines a tab within a [Tabs] component. +type TabDef struct { + Title DynamicString `json:"title"` + Child string `json:"child"` +} + +// ChoiceOption defines a selectable option within a [ChoicePicker] component. +type ChoiceOption struct { + Label DynamicString `json:"label"` + Value string `json:"value"` +} + +// AudioPlayerComponent holds the component-specific fields for a AudioPlayer. +type AudioPlayerComponent struct { + Description *DynamicString `json:"description,omitempty"` + URL DynamicString `json:"url"` +} + +// ButtonComponent holds the component-specific fields for a Button. +type ButtonComponent struct { + Action Action `json:"action"` + Child string `json:"child"` + Variant ButtonVariant `json:"variant,omitempty"` +} + +// CardComponent holds the component-specific fields for a Card. +type CardComponent struct { + Child string `json:"child"` +} + +// CheckBoxComponent holds the component-specific fields for a CheckBox. +type CheckBoxComponent struct { + Label DynamicString `json:"label"` + Value DynamicBoolean `json:"value"` +} + +// ChoicePickerComponent holds the component-specific fields for a ChoicePicker. +type ChoicePickerComponent struct { + DisplayStyle ChoicePickerDisplayStyle `json:"displayStyle,omitempty"` + Filterable *bool `json:"filterable,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Options []ChoiceOption `json:"options"` + Value DynamicStringList `json:"value"` + Variant ChoicePickerVariant `json:"variant,omitempty"` +} + +// ColumnComponent holds the component-specific fields for a Column. +type ColumnComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// DateTimeInputComponent holds the component-specific fields for a DateTimeInput. +type DateTimeInputComponent struct { + EnableDate *bool `json:"enableDate,omitempty"` + EnableTime *bool `json:"enableTime,omitempty"` + Label *DynamicString `json:"label,omitempty"` + Max *DynamicString `json:"max,omitempty"` + Min *DynamicString `json:"min,omitempty"` + Value DynamicString `json:"value"` +} + +// DividerComponent holds the component-specific fields for a Divider. +type DividerComponent struct { + Axis DividerAxis `json:"axis,omitempty"` +} + +// IconComponent holds the component-specific fields for a Icon. +type IconComponent struct { + Name IconNameOrPath `json:"name"` +} + +// ImageComponent holds the component-specific fields for a Image. +type ImageComponent struct { + Description *DynamicString `json:"description,omitempty"` + Fit ImageFit `json:"fit,omitempty"` + URL DynamicString `json:"url"` + Variant ImageVariant `json:"variant,omitempty"` +} + +// ListComponent holds the component-specific fields for a List. +type ListComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Direction ListDirection `json:"direction,omitempty"` +} + +// ModalComponent holds the component-specific fields for a Modal. +type ModalComponent struct { + Content string `json:"content"` + Trigger string `json:"trigger"` +} + +// RowComponent holds the component-specific fields for a Row. +type RowComponent struct { + Align LayoutAlign `json:"align,omitempty"` + Children ChildList `json:"children"` + Justify LayoutJustify `json:"justify,omitempty"` +} + +// SliderComponent holds the component-specific fields for a Slider. +type SliderComponent struct { + Label *DynamicString `json:"label,omitempty"` + Max float64 `json:"max"` + Min *float64 `json:"min,omitempty"` + Value DynamicNumber `json:"value"` +} + +// TabsComponent holds the component-specific fields for a Tabs. +type TabsComponent struct { + Tabs []TabDef `json:"tabs"` +} + +// TextComponent holds the component-specific fields for a Text. +type TextComponent struct { + Text DynamicString `json:"text"` + Variant TextVariant `json:"variant,omitempty"` +} + +// TextFieldComponent holds the component-specific fields for a TextField. +type TextFieldComponent struct { + Label DynamicString `json:"label"` + ValidationRegexp string `json:"validationRegexp,omitempty"` + Value *DynamicString `json:"value,omitempty"` + Variant TextFieldVariant `json:"variant,omitempty"` +} + +// VideoComponent holds the component-specific fields for a Video. +type VideoComponent struct { + URL DynamicString `json:"url"` +} diff --git a/agent_sdks/go/a2ui/v091/zz_component_marshal.go b/agent_sdks/go/a2ui/v091/zz_component_marshal.go new file mode 100644 index 0000000000..42411c47f9 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_component_marshal.go @@ -0,0 +1,200 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +import ( + "encoding/json" + "fmt" +) + +// MarshalJSON encodes a [Component] as a flat JSON object with the "component" +// discriminator, common fields, and component-specific fields merged together. +func (c Component) MarshalJSON() ([]byte, error) { + type common struct { + ComponentType string `json:"component"` + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + componentType, specific, count := c.componentData() + switch count { + case 0: + return nil, fmt.Errorf("a2ui: component has no concrete type set") + case 1: + default: + return nil, fmt.Errorf("a2ui: component has multiple concrete types set") + } + cm := common{ + ComponentType: componentType, + ID: c.ID, + Accessibility: c.Accessibility, + Weight: c.Weight, + Checks: c.Checks, + } + + commonBytes, err := json.Marshal(cm) + if err != nil { + return nil, err + } + + specificBytes, err := json.Marshal(specific) + if err != nil { + return nil, err + } + + // Merge: overwrite common with specific fields. + if len(specificBytes) <= 2 { // "{}" or empty + return commonBytes, nil + } + // Remove leading '{' from specific, append to common. + merged := make([]byte, 0, len(commonBytes)+len(specificBytes)) + merged = append(merged, commonBytes[:len(commonBytes)-1]...) // drop trailing '}' + merged = append(merged, ',') + merged = append(merged, specificBytes[1:]...) // drop leading '{' + return merged, nil +} + +// UnmarshalJSON decodes a flat JSON object into a [Component], using the +// "component" field as the discriminator. +func (c *Component) UnmarshalJSON(data []byte) error { + // Read the discriminator. + var disc struct { + ComponentType string `json:"component"` + } + if err := json.Unmarshal(data, &disc); err != nil { + return err + } + + // Unmarshal common fields. + type commonOnly struct { + ID string `json:"id"` + Accessibility *AccessibilityAttributes `json:"accessibility,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Checks []CheckRule `json:"checks,omitempty"` + } + var cm commonOnly + if err := json.Unmarshal(data, &cm); err != nil { + return err + } + *c = Component{} + c.ID = cm.ID + c.Accessibility = cm.Accessibility + c.Weight = cm.Weight + c.Checks = cm.Checks + + // Unmarshal component-specific fields. + switch disc.ComponentType { + case "AudioPlayer": + var v AudioPlayerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.AudioPlayer = &v + case "Button": + var v ButtonComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Button = &v + case "Card": + var v CardComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Card = &v + case "CheckBox": + var v CheckBoxComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.CheckBox = &v + case "ChoicePicker": + var v ChoicePickerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.ChoicePicker = &v + case "Column": + var v ColumnComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Column = &v + case "DateTimeInput": + var v DateTimeInputComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.DateTimeInput = &v + case "Divider": + var v DividerComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Divider = &v + case "Icon": + var v IconComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Icon = &v + case "Image": + var v ImageComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Image = &v + case "List": + var v ListComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.List = &v + case "Modal": + var v ModalComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Modal = &v + case "Row": + var v RowComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Row = &v + case "Slider": + var v SliderComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Slider = &v + case "Tabs": + var v TabsComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Tabs = &v + case "Text": + var v TextComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Text = &v + case "TextField": + var v TextFieldComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.TextField = &v + case "Video": + var v VideoComponent + if err := json.Unmarshal(data, &v); err != nil { + return err + } + c.Video = &v + default: + return fmt.Errorf("unknown component type: %s", disc.ComponentType) + } + return nil +} diff --git a/agent_sdks/go/a2ui/v091/zz_enum.go b/agent_sdks/go/a2ui/v091/zz_enum.go new file mode 100644 index 0000000000..c315973067 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_enum.go @@ -0,0 +1,129 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +// ReturnType is the expected return type of a function call. +type ReturnType string + +const ( + ReturnTypeString ReturnType = "string" + ReturnTypeNumber ReturnType = "number" + ReturnTypeBoolean ReturnType = "boolean" + ReturnTypeArray ReturnType = "array" + ReturnTypeObject ReturnType = "object" + ReturnTypeAny ReturnType = "any" + ReturnTypeVoid ReturnType = "void" +) + +// IconName identifies a built-in icon. +type IconName string + +// ButtonVariant defines the allowed values for the ButtonVariant enum. +type ButtonVariant string + +const ( + ButtonVariantDefault ButtonVariant = "default" + ButtonVariantPrimary ButtonVariant = "primary" + ButtonVariantBorderless ButtonVariant = "borderless" +) + +// ChoicePickerDisplayStyle defines the allowed values for the ChoicePickerDisplayStyle enum. +type ChoicePickerDisplayStyle string + +const ( + ChoicePickerDisplayStyleCheckbox ChoicePickerDisplayStyle = "checkbox" + ChoicePickerDisplayStyleChips ChoicePickerDisplayStyle = "chips" +) + +// ChoicePickerVariant defines the allowed values for the ChoicePickerVariant enum. +type ChoicePickerVariant string + +const ( + ChoicePickerVariantMultipleSelection ChoicePickerVariant = "multipleSelection" + ChoicePickerVariantMutuallyExclusive ChoicePickerVariant = "mutuallyExclusive" +) + +// DividerAxis defines the allowed values for the DividerAxis enum. +type DividerAxis string + +const ( + DividerAxisHorizontal DividerAxis = "horizontal" + DividerAxisVertical DividerAxis = "vertical" +) + +// ImageFit defines the allowed values for the ImageFit enum. +type ImageFit string + +const ( + ImageFitContain ImageFit = "contain" + ImageFitCover ImageFit = "cover" + ImageFitFill ImageFit = "fill" + ImageFitNone ImageFit = "none" + ImageFitScaleDown ImageFit = "scaleDown" +) + +// ImageVariant defines the allowed values for the ImageVariant enum. +type ImageVariant string + +const ( + ImageVariantIcon ImageVariant = "icon" + ImageVariantAvatar ImageVariant = "avatar" + ImageVariantSmallFeature ImageVariant = "smallFeature" + ImageVariantMediumFeature ImageVariant = "mediumFeature" + ImageVariantLargeFeature ImageVariant = "largeFeature" + ImageVariantHeader ImageVariant = "header" +) + +// LayoutAlign defines the allowed values for the LayoutAlign enum. +type LayoutAlign string + +const ( + LayoutAlignCenter LayoutAlign = "center" + LayoutAlignEnd LayoutAlign = "end" + LayoutAlignStart LayoutAlign = "start" + LayoutAlignStretch LayoutAlign = "stretch" +) + +// LayoutJustify defines the allowed values for the LayoutJustify enum. +type LayoutJustify string + +const ( + LayoutJustifyStart LayoutJustify = "start" + LayoutJustifyCenter LayoutJustify = "center" + LayoutJustifyEnd LayoutJustify = "end" + LayoutJustifySpaceBetween LayoutJustify = "spaceBetween" + LayoutJustifySpaceAround LayoutJustify = "spaceAround" + LayoutJustifySpaceEvenly LayoutJustify = "spaceEvenly" + LayoutJustifyStretch LayoutJustify = "stretch" +) + +// ListDirection defines the allowed values for the ListDirection enum. +type ListDirection string + +const ( + ListDirectionVertical ListDirection = "vertical" + ListDirectionHorizontal ListDirection = "horizontal" +) + +// TextFieldVariant defines the allowed values for the TextFieldVariant enum. +type TextFieldVariant string + +const ( + TextFieldVariantLongText TextFieldVariant = "longText" + TextFieldVariantNumber TextFieldVariant = "number" + TextFieldVariantShortText TextFieldVariant = "shortText" + TextFieldVariantObscured TextFieldVariant = "obscured" +) + +// TextVariant defines the allowed values for the TextVariant enum. +type TextVariant string + +const ( + TextVariantH1 TextVariant = "h1" + TextVariantH2 TextVariant = "h2" + TextVariantH3 TextVariant = "h3" + TextVariantH4 TextVariant = "h4" + TextVariantH5 TextVariant = "h5" + TextVariantCaption TextVariant = "caption" + TextVariantBody TextVariant = "body" +) diff --git a/agent_sdks/go/a2ui/v091/zz_function.go b/agent_sdks/go/a2ui/v091/zz_function.go new file mode 100644 index 0000000000..95ea8be074 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_function.go @@ -0,0 +1,188 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +// And creates a function call for "and". +// Performs a logical AND operation on a list of boolean values. +func And(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "and", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Email creates a function call for "email". +// Checks that the value is a valid email address. +func Email(value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "email", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// FormatCurrency creates a function call for "formatCurrency". +// Formats a number as a currency string. +func FormatCurrency(currency DynamicString, decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatCurrency", + Args: map[string]any{ + "currency": currency, + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatDate creates a function call for "formatDate". +// Formats a timestamp into a string using a pattern. +func FormatDate(format DynamicString, value DynamicValue) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatDate", + Args: map[string]any{ + "format": format, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatNumber creates a function call for "formatNumber". +// Formats a number with the specified grouping and decimal precision. +func FormatNumber(decimals DynamicNumber, grouping DynamicBoolean, value DynamicNumber) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatNumber", + Args: map[string]any{ + "decimals": decimals, + "grouping": grouping, + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// FormatString creates a function call for "formatString". +// Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\${`. +func FormatString(value DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "formatString", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeString, + }} +} + +// Length creates a function call for "length". +// Checks string length constraints. +func Length(max int, min int, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "length", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Not creates a function call for "not". +// Performs a logical NOT operation on a boolean value. +func Not(value DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "not", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Numeric creates a function call for "numeric". +// Checks numeric range constraints. +func Numeric(max float64, min float64, value DynamicNumber) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "numeric", + Args: map[string]any{ + "max": max, + "min": min, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// OpenURL creates a function call for "openUrl". +// Opens the specified URL in a browser or handler. This function has no return value. +func OpenURL(url string) Action { + return Action{FunctionCall: &FunctionCall{ + Call: "openUrl", + Args: map[string]any{ + "url": url, + }, + ReturnType: ReturnTypeVoid, + }} +} + +// Or creates a function call for "or". +// Performs a logical OR operation on a list of boolean values. +func Or(values []DynamicBoolean) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "or", + Args: map[string]any{ + "values": values, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Pluralize creates a function call for "pluralize". +// Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'. +func Pluralize(few DynamicString, many DynamicString, one DynamicString, other DynamicString, two DynamicString, value DynamicNumber, zero DynamicString) DynamicString { + return DynamicString{FunctionCall: &FunctionCall{ + Call: "pluralize", + Args: map[string]any{ + "few": few, + "many": many, + "one": one, + "other": other, + "two": two, + "value": value, + "zero": zero, + }, + ReturnType: ReturnTypeString, + }} +} + +// Regex creates a function call for "regex". +// Checks that the value matches a regular expression string. +func Regex(pattern string, value DynamicString) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "regex", + Args: map[string]any{ + "pattern": pattern, + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} + +// Required creates a function call for "required". +// Checks that the value is not null, undefined, or empty. +func Required(value DynamicValue) DynamicBoolean { + return DynamicBoolean{FunctionCall: &FunctionCall{ + Call: "required", + Args: map[string]any{ + "value": value, + }, + ReturnType: ReturnTypeBoolean, + }} +} diff --git a/agent_sdks/go/a2ui/v091/zz_icon.go b/agent_sdks/go/a2ui/v091/zz_icon.go new file mode 100644 index 0000000000..4f6d2b9a14 --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_icon.go @@ -0,0 +1,66 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +// Well-known icon names. +const ( + IconAccountCircle IconName = "accountCircle" + IconAdd IconName = "add" + IconArrowBack IconName = "arrowBack" + IconArrowForward IconName = "arrowForward" + IconAttachFile IconName = "attachFile" + IconCalendarToday IconName = "calendarToday" + IconCall IconName = "call" + IconCamera IconName = "camera" + IconCheck IconName = "check" + IconClose IconName = "close" + IconDelete IconName = "delete" + IconDownload IconName = "download" + IconEdit IconName = "edit" + IconEvent IconName = "event" + IconError IconName = "error" + IconFastForward IconName = "fastForward" + IconFavorite IconName = "favorite" + IconFavoriteOff IconName = "favoriteOff" + IconFolder IconName = "folder" + IconHelp IconName = "help" + IconHome IconName = "home" + IconInfo IconName = "info" + IconLocationOn IconName = "locationOn" + IconLock IconName = "lock" + IconLockOpen IconName = "lockOpen" + IconMail IconName = "mail" + IconMenu IconName = "menu" + IconMoreVert IconName = "moreVert" + IconMoreHoriz IconName = "moreHoriz" + IconNotificationsOff IconName = "notificationsOff" + IconNotifications IconName = "notifications" + IconPause IconName = "pause" + IconPayment IconName = "payment" + IconPerson IconName = "person" + IconPhone IconName = "phone" + IconPhoto IconName = "photo" + IconPlay IconName = "play" + IconPrint IconName = "print" + IconRefresh IconName = "refresh" + IconRewind IconName = "rewind" + IconSearch IconName = "search" + IconSend IconName = "send" + IconSettings IconName = "settings" + IconShare IconName = "share" + IconShoppingCart IconName = "shoppingCart" + IconSkipNext IconName = "skipNext" + IconSkipPrevious IconName = "skipPrevious" + IconStar IconName = "star" + IconStarHalf IconName = "starHalf" + IconStarOff IconName = "starOff" + IconStop IconName = "stop" + IconUpload IconName = "upload" + IconVisibility IconName = "visibility" + IconVisibilityOff IconName = "visibilityOff" + IconVolumeDown IconName = "volumeDown" + IconVolumeMute IconName = "volumeMute" + IconVolumeOff IconName = "volumeOff" + IconVolumeUp IconName = "volumeUp" + IconWarning IconName = "warning" +) diff --git a/agent_sdks/go/a2ui/v091/zz_wrapper.go b/agent_sdks/go/a2ui/v091/zz_wrapper.go new file mode 100644 index 0000000000..6e83a6602a --- /dev/null +++ b/agent_sdks/go/a2ui/v091/zz_wrapper.go @@ -0,0 +1,15 @@ +// Code generated by a2uigen; DO NOT EDIT. + +package v091 + +// ClientMessageListWrapper wraps a list of [ClientMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ClientMessageListWrapper struct { + Messages []ClientMessage `json:"messages"` +} + +// ServerMessageListWrapper wraps a list of [ServerMessage] in a {"messages": [...]} +// envelope for transports that require a top-level JSON object. +type ServerMessageListWrapper struct { + Messages []ServerMessage `json:"messages"` +} diff --git a/agent_sdks/go/a2uiadk/doc.go b/agent_sdks/go/a2uiadk/doc.go new file mode 100644 index 0000000000..1863e42b0d --- /dev/null +++ b/agent_sdks/go/a2uiadk/doc.go @@ -0,0 +1,2 @@ +// Package a2uiadk provides transport-neutral helpers for ADK-style A2UI tools. +package a2uiadk diff --git a/agent_sdks/go/a2uiadk/example_test.go b/agent_sdks/go/a2uiadk/example_test.go new file mode 100644 index 0000000000..e411c05b31 --- /dev/null +++ b/agent_sdks/go/a2uiadk/example_test.go @@ -0,0 +1,25 @@ +package a2uiadk_test + +import ( + "fmt" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uiadk" + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uischema" +) + +func ExampleSendA2UIJSONToClientTool_Run() { + cfg, _ := a2uischema.BasicCatalogConfig(a2uischema.Version09) + manager, _ := a2uischema.NewSchemaManager(a2uischema.Version09, []a2uischema.CatalogConfig{cfg}, false) + catalog, _ := manager.SelectedCatalog(nil, nil, nil) + tool := a2uiadk.NewSendA2UIJSONToClientTool(catalog.Validator()) + + result := tool.Run(map[string]any{ + a2uiadk.A2UIJSONArgName: `[ + {"version":"v0.9","createSurface":{"surfaceId":"demo","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}}, + {"version":"v0.9","updateComponents":{"surfaceId":"demo","components":[{"component":"Text","id":"root","text":"hello"}]}} + ]`, + }, nil) + _, ok := result[a2uiadk.ValidatedJSONKey] + fmt.Println(ok) + // Output: true +} diff --git a/agent_sdks/go/a2uiadk/tool.go b/agent_sdks/go/a2uiadk/tool.go new file mode 100644 index 0000000000..084040747f --- /dev/null +++ b/agent_sdks/go/a2uiadk/tool.go @@ -0,0 +1,105 @@ +package a2uiadk + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uischema" + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uistream" +) + +const ( + ToolName = "send_a2ui_json_to_client" + ValidatedJSONKey = "validated_a2ui_json" + ToolErrorKey = "error" + A2UIJSONArgName = "a2ui_json" +) + +// ToolDeclaration describes the A2UI tool for adapters that expose tools to an LLM. +type ToolDeclaration struct { + Name string + Description string + Parameters map[string]any + Required []string +} + +// ToolContext carries mutable execution state for the tool call. +type ToolContext struct { + SkipSummarization bool +} + +// SendA2UIJSONToClientTool validates A2UI JSON supplied by an LLM tool call. +type SendA2UIJSONToClientTool struct { + Validator *a2uischema.Validator +} + +// NewSendA2UIJSONToClientTool returns a tool that validates against validator. +func NewSendA2UIJSONToClientTool(validator *a2uischema.Validator) SendA2UIJSONToClientTool { + return SendA2UIJSONToClientTool{Validator: validator} +} + +// Declaration returns a transport-neutral function declaration for this tool. +func (t SendA2UIJSONToClientTool) Declaration() ToolDeclaration { + return ToolDeclaration{ + Name: ToolName, + Description: "Sends A2UI JSON to the client to render rich UI natively. Always prefer this over returning raw JSON.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + A2UIJSONArgName: map[string]any{ + "type": "string", + "description": "The A2UI JSON payload to send to the client.", + }, + }, + }, + Required: []string{A2UIJSONArgName}, + } +} + +// ProcessInstructions returns schema and examples text to append to LLM instructions. +func ProcessInstructions(catalog *a2uischema.Catalog, examples string) ([]string, error) { + if catalog == nil { + return nil, fmt.Errorf("a2uiadk: nil catalog") + } + instructions, err := catalog.RenderAsLLMInstructions() + if err != nil { + return nil, err + } + if strings.TrimSpace(examples) == "" { + return []string{instructions}, nil + } + return []string{instructions, examples}, nil +} + +// Run validates the a2ui_json argument and returns a result map. +func (t SendA2UIJSONToClientTool) Run(args map[string]any, ctx *ToolContext) map[string]any { + payload, ok := args[A2UIJSONArgName].(string) + if !ok || payload == "" { + return toolError(fmt.Errorf("missing required arg %s", A2UIJSONArgName)) + } + if t.Validator == nil { + return toolError(fmt.Errorf("nil validator")) + } + objects, err := a2uistream.FixPayload(payload) + if err != nil { + return toolError(err) + } + data, err := json.Marshal(objects) + if err != nil { + return toolError(err) + } + if err := t.Validator.ValidateJSON(data); err != nil { + return toolError(err) + } + if ctx != nil { + ctx.SkipSummarization = true + } + return map[string]any{ValidatedJSONKey: objects} +} + +func toolError(err error) map[string]any { + return map[string]any{ + ToolErrorKey: fmt.Sprintf("Failed to call A2UI tool %s: %v", ToolName, err), + } +} diff --git a/agent_sdks/go/a2uiadk/tool_test.go b/agent_sdks/go/a2uiadk/tool_test.go new file mode 100644 index 0000000000..e3cdc19032 --- /dev/null +++ b/agent_sdks/go/a2uiadk/tool_test.go @@ -0,0 +1,93 @@ +package a2uiadk + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uischema" +) + +func TestSendA2UIJSONToClientToolRun(t *testing.T) { + catalog := testCatalog(t) + tool := NewSendA2UIJSONToClientTool(catalog.Validator()) + ctx := &ToolContext{} + result := tool.Run(map[string]any{ + A2UIJSONArgName: `[ + {"version":"v0.9","createSurface":{"surfaceId":"dummy-surface","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}}, + {"version":"v0.9","updateComponents":{"surfaceId":"dummy-surface","components":[{"component":"Text","id":"root","text":"hello"}]}} + ]`, + }, ctx) + if _, ok := result[ToolErrorKey]; ok { + t.Fatalf("Run returned error: %#v", result) + } + if !ctx.SkipSummarization { + t.Fatal("SkipSummarization = false, want true") + } + payload, ok := result[ValidatedJSONKey].([]map[string]any) + if !ok { + t.Fatalf("validated payload type = %T", result[ValidatedJSONKey]) + } + if len(payload) != 2 { + t.Fatalf("validated payload length = %d, want 2", len(payload)) + } +} + +func TestSendA2UIJSONToClientToolRunMissingArg(t *testing.T) { + tool := NewSendA2UIJSONToClientTool(testCatalog(t).Validator()) + result := tool.Run(map[string]any{"wrong_arg": "b"}, nil) + errText, ok := result[ToolErrorKey].(string) + if !ok { + t.Fatalf("missing error in result %#v", result) + } + if !strings.Contains(errText, "missing required arg a2ui_json") { + t.Fatalf("error = %q", errText) + } +} + +func TestSendA2UIJSONToClientToolDeclaration(t *testing.T) { + decl := NewSendA2UIJSONToClientTool(nil).Declaration() + if decl.Name != ToolName { + t.Fatalf("Name = %q, want %q", decl.Name, ToolName) + } + if len(decl.Required) != 1 || decl.Required[0] != A2UIJSONArgName { + t.Fatalf("Required = %#v", decl.Required) + } + data, err := json.Marshal(decl.Parameters) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), A2UIJSONArgName) { + t.Fatalf("parameters missing %q: %s", A2UIJSONArgName, data) + } +} + +func TestProcessInstructions(t *testing.T) { + instructions, err := ProcessInstructions(testCatalog(t), "examples") + if err != nil { + t.Fatal(err) + } + if len(instructions) != 2 { + t.Fatalf("instructions length = %d, want 2", len(instructions)) + } + if !strings.Contains(instructions[0], a2uischema.A2UISchemaBlockStart) { + t.Fatalf("missing schema block: %q", instructions[0]) + } +} + +func testCatalog(t *testing.T) *a2uischema.Catalog { + t.Helper() + cfg, err := a2uischema.BasicCatalogConfig(a2uischema.Version09) + if err != nil { + t.Fatal(err) + } + manager, err := a2uischema.NewSchemaManager(a2uischema.Version09, []a2uischema.CatalogConfig{cfg}, false) + if err != nil { + t.Fatal(err) + } + catalog, err := manager.SelectedCatalog(nil, nil, nil) + if err != nil { + t.Fatal(err) + } + return catalog +} diff --git a/agent_sdks/go/a2uibuild/children.go b/agent_sdks/go/a2uibuild/children.go new file mode 100644 index 0000000000..b86dd4de19 --- /dev/null +++ b/agent_sdks/go/a2uibuild/children.go @@ -0,0 +1,8 @@ +package a2uibuild + +import "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + +// Children returns a static child list containing ids. +func Children(ids ...string) a2ui.ChildList { + return a2ui.ChildList{IDs: append([]string(nil), ids...)} +} diff --git a/agent_sdks/go/a2uibuild/doc.go b/agent_sdks/go/a2uibuild/doc.go new file mode 100644 index 0000000000..e689f598d4 --- /dev/null +++ b/agent_sdks/go/a2uibuild/doc.go @@ -0,0 +1,6 @@ +// Package a2uibuild provides convenience constructors and a value builder for the +// root A2UI compatibility API. +// +// The root API currently targets A2UI v0.9. Code that needs v0.10 message +// types should import the version package directly. +package a2uibuild diff --git a/agent_sdks/go/a2uibuild/example_test.go b/agent_sdks/go/a2uibuild/example_test.go new file mode 100644 index 0000000000..502a490455 --- /dev/null +++ b/agent_sdks/go/a2uibuild/example_test.go @@ -0,0 +1,23 @@ +package a2uibuild_test + +import ( + "encoding/json" + "fmt" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uibuild" +) + +func Example() { + s := a2uibuild.NewSurface("contact", "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"). + Add(a2uibuild.Column("root", a2uibuild.Children("greeting"))). + Add(a2uibuild.Text("greeting", a2ui.StringLiteral("Hello, world!"))) + + for _, msg := range s.Messages() { + data, _ := json.Marshal(msg) + fmt.Println(string(data)) + } + // Output: + // {"version":"v0.9","createSurface":{"surfaceId":"contact","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}} + // {"version":"v0.9","updateComponents":{"surfaceId":"contact","components":[{"component":"Column","id":"root","children":["greeting"]},{"component":"Text","id":"greeting","text":"Hello, world!"}]}} +} diff --git a/agent_sdks/go/a2uibuild/surface.go b/agent_sdks/go/a2uibuild/surface.go new file mode 100644 index 0000000000..a55c3d66b1 --- /dev/null +++ b/agent_sdks/go/a2uibuild/surface.go @@ -0,0 +1,93 @@ +package a2uibuild + +import ( + "maps" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" +) + +// Surface builds a complete A2UI surface as a sequence of server messages. +type Surface struct { + surfaceID string + catalogID string + theme *a2ui.Theme + components []a2ui.Component + data map[string]any + sendData bool +} + +// NewSurface returns a surface builder with the given surface and catalog IDs. +func NewSurface(surfaceID, catalogID string) Surface { + return Surface{ + surfaceID: surfaceID, + catalogID: catalogID, + } +} + +// WithTheme returns a surface with theme parameters set. +func (s Surface) WithTheme(t a2ui.Theme) Surface { + s.theme = &t + return s +} + +// WithSendDataModel returns a surface that requests client data in actions. +func (s Surface) WithSendDataModel() Surface { + s.sendData = true + return s +} + +// Add returns a surface with c appended. +func (s Surface) Add(c a2ui.Component) Surface { + s.components = append(slicesClone(s.components), c) + return s +} + +// WithData returns a surface with the initial data model set. +func (s Surface) WithData(data map[string]any) Surface { + s.data = maps.Clone(data) + return s +} + +// Messages returns the server messages needed to render this surface. +func (s Surface) Messages() []a2ui.ServerMessage { + var msgs []a2ui.ServerMessage + + msgs = append(msgs, a2ui.ServerMessage{ + Version: a2ui.Version, + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: s.surfaceID, + CatalogID: s.catalogID, + Theme: s.theme, + SendDataModel: s.sendData, + }, + }) + + if len(s.components) > 0 { + msgs = append(msgs, a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: s.surfaceID, + Components: slicesClone(s.components), + }, + }) + } + + if len(s.data) > 0 { + msgs = append(msgs, a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateDataModel: &a2ui.UpdateDataModel{ + SurfaceID: s.surfaceID, + Value: maps.Clone(s.data), + }, + }) + } + + return msgs +} + +func slicesClone[S ~[]E, E any](s S) S { + if s == nil { + return nil + } + return append(S(nil), s...) +} diff --git a/agent_sdks/go/a2uibuild/surface_test.go b/agent_sdks/go/a2uibuild/surface_test.go new file mode 100644 index 0000000000..cc09302fb0 --- /dev/null +++ b/agent_sdks/go/a2uibuild/surface_test.go @@ -0,0 +1,58 @@ +package a2uibuild + +import ( + "encoding/json" + "testing" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" +) + +func TestSurfaceMessagesMarshal(t *testing.T) { + s := NewSurface("contact", "catalog"). + Add(Column("root", Children("greeting"))). + Add(Text("greeting", a2ui.StringLiteral("Hello, world!"))) + + for i, msg := range s.Messages() { + if _, err := json.Marshal(msg); err != nil { + t.Fatalf("message[%d]: marshal: %v", i, err) + } + } +} + +func TestChildrenClonesIDs(t *testing.T) { + ids := []string{"a", "b"} + children := Children(ids...) + ids[0] = "changed" + if children.IDs[0] != "a" { + t.Fatalf("Children aliases input slice") + } +} + +func TestSurfaceBuilderDoesNotMutateBase(t *testing.T) { + base := NewSurface("contact", "catalog") + left := base.Add(Text("left", a2ui.StringLiteral("left"))) + right := base.Add(Text("right", a2ui.StringLiteral("right"))) + + if got := len(base.Messages()); got != 1 { + t.Fatalf("base messages = %d, want 1", got) + } + if got := componentID(t, left); got != "left" { + t.Fatalf("left component = %q, want left", got) + } + if got := componentID(t, right); got != "right" { + t.Fatalf("right component = %q, want right", got) + } +} + +func componentID(t *testing.T, s Surface) string { + t.Helper() + msgs := s.Messages() + if len(msgs) != 2 { + t.Fatalf("messages = %d, want 2", len(msgs)) + } + components := msgs[1].UpdateComponents.Components + if len(components) != 1 || components[0].Text == nil { + t.Fatalf("unexpected components: %+v", components) + } + return components[0].ID +} diff --git a/agent_sdks/go/a2uibuild/zz_builders.go b/agent_sdks/go/a2uibuild/zz_builders.go new file mode 100644 index 0000000000..d83ce318b8 --- /dev/null +++ b/agent_sdks/go/a2uibuild/zz_builders.go @@ -0,0 +1,189 @@ +// Code generated by a2uigen; DO NOT EDIT. + +// Package a2uibuild provides convenience constructors for A2UI components. +package a2uibuild + +import "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + +// AudioPlayer creates a new [a2ui.Component] of type AudioPlayer with the given id. +func AudioPlayer(id string, url a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + AudioPlayer: &a2ui.AudioPlayerComponent{ + URL: url, + }, + } +} + +// Button creates a new [a2ui.Component] of type Button with the given id. +func Button(id string, action a2ui.Action, child string) a2ui.Component { + return a2ui.Component{ + ID: id, + Button: &a2ui.ButtonComponent{ + Action: action, + Child: child, + }, + } +} + +// Card creates a new [a2ui.Component] of type Card with the given id. +func Card(id string, child string) a2ui.Component { + return a2ui.Component{ + ID: id, + Card: &a2ui.CardComponent{ + Child: child, + }, + } +} + +// CheckBox creates a new [a2ui.Component] of type CheckBox with the given id. +func CheckBox(id string, label a2ui.DynamicString, value a2ui.DynamicBoolean) a2ui.Component { + return a2ui.Component{ + ID: id, + CheckBox: &a2ui.CheckBoxComponent{ + Label: label, + Value: value, + }, + } +} + +// ChoicePicker creates a new [a2ui.Component] of type ChoicePicker with the given id. +func ChoicePicker(id string, options []a2ui.ChoiceOption, value a2ui.DynamicStringList) a2ui.Component { + return a2ui.Component{ + ID: id, + ChoicePicker: &a2ui.ChoicePickerComponent{ + Options: options, + Value: value, + }, + } +} + +// Column creates a new [a2ui.Component] of type Column with the given id. +func Column(id string, children a2ui.ChildList) a2ui.Component { + return a2ui.Component{ + ID: id, + Column: &a2ui.ColumnComponent{ + Children: children, + }, + } +} + +// DateTimeInput creates a new [a2ui.Component] of type DateTimeInput with the given id. +func DateTimeInput(id string, value a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + DateTimeInput: &a2ui.DateTimeInputComponent{ + Value: value, + }, + } +} + +// Divider creates a new [a2ui.Component] of type Divider with the given id. +func Divider(id string) a2ui.Component { + return a2ui.Component{ + ID: id, + Divider: &a2ui.DividerComponent{}, + } +} + +// Icon creates a new [a2ui.Component] of type Icon with the given id. +func Icon(id string, name a2ui.IconNameOrPath) a2ui.Component { + return a2ui.Component{ + ID: id, + Icon: &a2ui.IconComponent{ + Name: name, + }, + } +} + +// Image creates a new [a2ui.Component] of type Image with the given id. +func Image(id string, url a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + Image: &a2ui.ImageComponent{ + URL: url, + }, + } +} + +// List creates a new [a2ui.Component] of type List with the given id. +func List(id string, children a2ui.ChildList) a2ui.Component { + return a2ui.Component{ + ID: id, + List: &a2ui.ListComponent{ + Children: children, + }, + } +} + +// Modal creates a new [a2ui.Component] of type Modal with the given id. +func Modal(id string, content string, trigger string) a2ui.Component { + return a2ui.Component{ + ID: id, + Modal: &a2ui.ModalComponent{ + Content: content, + Trigger: trigger, + }, + } +} + +// Row creates a new [a2ui.Component] of type Row with the given id. +func Row(id string, children a2ui.ChildList) a2ui.Component { + return a2ui.Component{ + ID: id, + Row: &a2ui.RowComponent{ + Children: children, + }, + } +} + +// Slider creates a new [a2ui.Component] of type Slider with the given id. +func Slider(id string, max float64, value a2ui.DynamicNumber) a2ui.Component { + return a2ui.Component{ + ID: id, + Slider: &a2ui.SliderComponent{ + Max: max, + Value: value, + }, + } +} + +// Tabs creates a new [a2ui.Component] of type Tabs with the given id. +func Tabs(id string, tabs []a2ui.TabDef) a2ui.Component { + return a2ui.Component{ + ID: id, + Tabs: &a2ui.TabsComponent{ + Tabs: tabs, + }, + } +} + +// Text creates a new [a2ui.Component] of type Text with the given id. +func Text(id string, text a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + Text: &a2ui.TextComponent{ + Text: text, + }, + } +} + +// TextField creates a new [a2ui.Component] of type TextField with the given id. +func TextField(id string, label a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + TextField: &a2ui.TextFieldComponent{ + Label: label, + }, + } +} + +// Video creates a new [a2ui.Component] of type Video with the given id. +func Video(id string, url a2ui.DynamicString) a2ui.Component { + return a2ui.Component{ + ID: id, + Video: &a2ui.VideoComponent{ + URL: url, + }, + } +} diff --git a/agent_sdks/go/a2uischema/assets.go b/agent_sdks/go/a2uischema/assets.go new file mode 100644 index 0000000000..5f76721185 --- /dev/null +++ b/agent_sdks/go/a2uischema/assets.go @@ -0,0 +1,41 @@ +package a2uischema + +import _ "embed" + +var ( + //go:embed schemas/v0_9/server_to_client.json + serverToClientV09 []byte + + //go:embed schemas/v0_9/common_types.json + commonTypesV09 []byte + + //go:embed schemas/v0_9/basic_catalog.json + basicCatalogV09 []byte + + //go:embed schemas/v0_9/basic_catalog_rules.txt + basicCatalogRulesV09 string + + //go:embed schemas/v0_9_1/server_to_client.json + serverToClientV091 []byte + + //go:embed schemas/v0_9_1/common_types.json + commonTypesV091 []byte + + //go:embed schemas/v0_9_1/basic_catalog.json + basicCatalogV091 []byte + + //go:embed schemas/v0_9_1/basic_catalog_rules.txt + basicCatalogRulesV091 string + + //go:embed schemas/v0_10/server_to_client.json + serverToClientV010 []byte + + //go:embed schemas/v0_10/common_types.json + commonTypesV010 []byte + + //go:embed schemas/v0_10/basic_catalog.json + basicCatalogV010 []byte + + //go:embed schemas/v0_10/basic_catalog_rules.txt + basicCatalogRulesV010 string +) diff --git a/agent_sdks/go/a2uischema/catalog.go b/agent_sdks/go/a2uischema/catalog.go new file mode 100644 index 0000000000..8464fce98d --- /dev/null +++ b/agent_sdks/go/a2uischema/catalog.go @@ -0,0 +1,433 @@ +package a2uischema + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "strings" +) + +// CatalogConfig configures how a catalog is loaded. +type CatalogConfig struct { + Name string + Provider CatalogProvider + ExamplesPath string +} + +// CatalogConfigFromPath constructs a [CatalogConfig] from a file path. +func CatalogConfigFromPath(name, catalogPath, examplesPath string) CatalogConfig { + return CatalogConfig{ + Name: name, + Provider: FileSystemCatalogProvider(catalogPath), + ExamplesPath: examplesPath, + } +} + +// BasicCatalogConfig returns a [CatalogConfig] backed by embedded schemas. +func BasicCatalogConfig(version Version) (CatalogConfig, error) { + provider, err := BasicCatalogProvider(version) + if err != nil { + return CatalogConfig{}, err + } + return CatalogConfig{ + Name: "basic", + Provider: provider, + }, nil +} + +// Catalog is a processed catalog plus the schemas needed to reason about it. +type Catalog struct { + Version Version + Name string + ServerToClientSchema map[string]any + CommonTypesSchema map[string]any + CatalogSchema map[string]any +} + +// ID returns the catalog identifier. +func (c *Catalog) ID() (string, error) { + id, ok := c.CatalogSchema[CatalogIDKey].(string) + if !ok || id == "" { + return "", fmt.Errorf("schema: catalog %q missing catalogId", c.Name) + } + return id, nil +} + +// Validator returns a new validator for the catalog. +func (c *Catalog) Validator() *Validator { + return NewValidator(c) +} + +// WithPruning returns a copy of the catalog pruned to the requested components and messages. +func (c *Catalog) WithPruning(allowedComponents, allowedMessages []string) (*Catalog, error) { + if c == nil { + return nil, fmt.Errorf("schema: nil catalog") + } + serverSchema, commonSchema, catalogSchema, err := cloneCatalogSchemas(c) + if err != nil { + return nil, err + } + out := &Catalog{ + Version: c.Version, + Name: c.Name, + ServerToClientSchema: serverSchema, + CommonTypesSchema: commonSchema, + CatalogSchema: catalogSchema, + } + if len(allowedComponents) > 0 { + components, _ := out.CatalogSchema[CatalogComponentsKey].(map[string]any) + if components != nil { + filtered := make(map[string]any) + for _, name := range allowedComponents { + if value, ok := components[name]; ok { + filtered[name] = value + } + } + out.CatalogSchema[CatalogComponentsKey] = filtered + } + if defs, ok := out.CatalogSchema["$defs"].(map[string]any); ok { + if anyComponent, ok := defs["anyComponent"].(map[string]any); ok { + if oneOf, ok := anyComponent["oneOf"].([]any); ok { + filtered := oneOf[:0] + for _, item := range oneOf { + ref, _ := item.(map[string]any)["$ref"].(string) + if ref == "" { + filtered = append(filtered, item) + continue + } + name := ref[strings.LastIndex(ref, "/")+1:] + if slices.Contains(allowedComponents, name) { + filtered = append(filtered, item) + } + } + anyComponent["oneOf"] = filtered + } + } + } + } + if len(allowedMessages) > 0 { + defs, _ := out.ServerToClientSchema["$defs"].(map[string]any) + if defs != nil { + filteredDefs := make(map[string]any) + for _, name := range allowedMessages { + if value, ok := defs[name]; ok { + filteredDefs[name] = value + } + } + out.ServerToClientSchema["$defs"] = filteredDefs + } + if oneOf, ok := out.ServerToClientSchema["oneOf"].([]any); ok { + filtered := oneOf[:0] + for _, item := range oneOf { + ref, _ := item.(map[string]any)["$ref"].(string) + if ref == "" { + continue + } + name := ref[strings.LastIndex(ref, "/")+1:] + if slices.Contains(allowedMessages, name) { + filtered = append(filtered, item) + } + } + out.ServerToClientSchema["oneOf"] = filtered + } + } + return out, nil +} + +// RenderAsLLMInstructions renders the schemas as a schema block suitable for prompts. +func (c *Catalog) RenderAsLLMInstructions() (string, error) { + serverSchema, err := marshalIndented(c.ServerToClientSchema) + if err != nil { + return "", err + } + commonTypes, err := marshalIndented(c.CommonTypesSchema) + if err != nil { + return "", err + } + catalogSchema, err := marshalIndented(c.CatalogSchema) + if err != nil { + return "", err + } + var b strings.Builder + b.WriteString(A2UISchemaBlockStart) + b.WriteString("\n### Server To Client Schema:\n") + b.Write(serverSchema) + if len(commonTypes) > 0 && string(commonTypes) != "{}" { + b.WriteString("\n\n### Common Types Schema:\n") + b.Write(commonTypes) + } + b.WriteString("\n\n### Catalog Schema:\n") + b.Write(catalogSchema) + if rules, ok := embeddedCatalogRules(c); ok && strings.TrimSpace(rules) != "" { + b.WriteString("\n\n### Catalog Rules:\n") + b.WriteString(strings.TrimSpace(rules)) + } + b.WriteString("\n") + b.WriteString(A2UISchemaBlockEnd) + return b.String(), nil +} + +// LoadExamples loads `.json` examples from a path and optionally validates them. +// Path may name a file, a directory, or a glob pattern. +func (c *Catalog) LoadExamples(path string, validate bool) (string, error) { + if path == "" { + return "", nil + } + files, err := exampleFiles(path) + if err != nil { + return "", err + } + var blocks []string + validator := c.Validator() + for _, file := range files { + data, err := os.ReadFile(file) + if err != nil { + return "", err + } + if validate { + if err := validator.ValidateExample(data); err != nil { + return "", fmt.Errorf("schema: validate example %s: %w", file, err) + } + } + name := filepath.Base(file) + base := strings.TrimSuffix(name, filepath.Ext(name)) + blocks = append(blocks, fmt.Sprintf("---BEGIN %s---\n%s\n---END %s---", base, strings.TrimSpace(string(data)), base)) + } + return strings.Join(blocks, "\n\n"), nil +} + +func exampleFiles(path string) ([]string, error) { + if hasGlobMeta(path) { + return globExampleFiles(path) + } + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + if !info.IsDir() { + if strings.HasSuffix(info.Name(), ".json") { + return []string{path}, nil + } + return nil, nil + } + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + var files []string + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { + continue + } + files = append(files, filepath.Join(path, entry.Name())) + } + slices.Sort(files) + return files, nil +} + +func globExampleFiles(pattern string) ([]string, error) { + pattern = normalizeGlobPattern(pattern) + if strings.Contains(pattern, "**") { + return globStarExampleFiles(pattern) + } + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + matches = filterJSONFiles(matches) + slices.Sort(matches) + return matches, nil +} + +func globStarExampleFiles(pattern string) ([]string, error) { + i := strings.Index(pattern, "**") + root := pattern[:i] + if root == "" { + root = "." + } + root = strings.TrimRight(root, string(filepath.Separator)) + suffix := strings.TrimLeft(pattern[i+len("**"):], string(filepath.Separator)) + if suffix == "" { + suffix = "*" + } + var files []string + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if d.IsDir() || !strings.HasSuffix(d.Name(), ".json") { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + ok, err := matchGlobStarSuffix(suffix, rel) + if err != nil { + return err + } + if ok { + files = append(files, path) + } + return nil + }) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + slices.Sort(files) + return files, nil +} + +func matchGlobStarSuffix(suffix, rel string) (bool, error) { + if ok, err := filepath.Match(suffix, rel); ok || err != nil { + return ok, err + } + if !strings.Contains(suffix, string(filepath.Separator)) { + return filepath.Match(suffix, filepath.Base(rel)) + } + return false, nil +} + +func filterJSONFiles(paths []string) []string { + files := paths[:0] + for _, path := range paths { + info, err := os.Stat(path) + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".json") { + continue + } + files = append(files, path) + } + return files +} + +func hasGlobMeta(path string) bool { + return strings.ContainsAny(path, "*?[") +} + +func normalizeGlobPattern(pattern string) string { + return strings.ReplaceAll(pattern, "[!", "[^") +} + +func newCatalog(version Version, name string, serverToClientSchema, commonTypesSchema, catalogSchema []byte) (*Catalog, error) { + serverMap, err := unmarshalJSONMap(serverToClientSchema) + if err != nil { + return nil, fmt.Errorf("schema: decode server_to_client schema: %w", err) + } + commonMap, err := unmarshalJSONMap(commonTypesSchema) + if err != nil { + return nil, fmt.Errorf("schema: decode common_types schema: %w", err) + } + catalogMap, err := unmarshalJSONMap(catalogSchema) + if err != nil { + return nil, fmt.Errorf("schema: decode catalog schema: %w", err) + } + return &Catalog{ + Version: version, + Name: name, + ServerToClientSchema: serverMap, + CommonTypesSchema: commonMap, + CatalogSchema: catalogMap, + }, nil +} + +func embeddedCatalogRules(c *Catalog) (string, bool) { + if c == nil { + return "", false + } + id, err := c.ID() + if err != nil { + return "", false + } + switch { + case c.Version == Version09 && id == "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json": + return basicCatalogRulesV09, true + case c.Version == Version091 && id == "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json": + return basicCatalogRulesV091, true + case c.Version == Version010 && id == "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json": + return basicCatalogRulesV010, true + } + return "", false +} + +func marshalIndented(v any) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return nil, err + } + return bytes.TrimSpace(buf.Bytes()), nil +} + +func unmarshalJSONMap(data []byte) (map[string]any, error) { + if len(bytes.TrimSpace(data)) == 0 { + return map[string]any{}, nil + } + var out map[string]any + if err := json.Unmarshal(data, &out); err != nil { + return nil, err + } + return out, nil +} + +func cloneCatalogSchemas(c *Catalog) (serverSchema, commonSchema, catalogSchema map[string]any, err error) { + if c == nil { + return nil, nil, nil, fmt.Errorf("schema: nil catalog") + } + serverSchema, err = cloneJSONMap(c.ServerToClientSchema) + if err != nil { + return nil, nil, nil, fmt.Errorf("schema: clone server_to_client schema: %w", err) + } + commonSchema, err = cloneJSONMap(c.CommonTypesSchema) + if err != nil { + return nil, nil, nil, fmt.Errorf("schema: clone common_types schema: %w", err) + } + catalogSchema, err = cloneJSONMap(c.CatalogSchema) + if err != nil { + return nil, nil, nil, fmt.Errorf("schema: clone catalog schema: %w", err) + } + return serverSchema, commonSchema, catalogSchema, nil +} + +func cloneJSONMap(m map[string]any) (map[string]any, error) { + if m == nil { + return nil, nil + } + out := make(map[string]any, len(m)) + for k, v := range m { + out[k] = cloneJSONValue(v) + } + return out, nil +} + +func cloneJSONValue(v any) any { + switch v := v.(type) { + case map[string]any: + out := make(map[string]any, len(v)) + for k, elem := range v { + out[k] = cloneJSONValue(elem) + } + return out + case []any: + out := make([]any, len(v)) + for i, elem := range v { + out[i] = cloneJSONValue(elem) + } + return out + default: + return v + } +} diff --git a/agent_sdks/go/a2uischema/catalog_conformance_test.go b/agent_sdks/go/a2uischema/catalog_conformance_test.go new file mode 100644 index 0000000000..d1d7192d67 --- /dev/null +++ b/agent_sdks/go/a2uischema/catalog_conformance_test.go @@ -0,0 +1,121 @@ +package a2uischema + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCatalogLoadExamplesConformance(t *testing.T) { + catalog := &Catalog{ + Version: Version09, + Name: "test", + ServerToClientSchema: map[string]any{}, + CommonTypesSchema: map[string]any{}, + CatalogSchema: map[string]any{CatalogIDKey: "basic"}, + } + root := makeLoadExamplesFixtures(t) + tests := []struct { + name string + path string + want string + }{ + { + name: "empty", + path: "", + }, + { + name: "missing", + path: root + "/missing", + }, + { + name: "directory", + path: root + "/basic", + want: "---BEGIN example1---\n" + + `[{"beginRendering": {"surfaceId": "id"}}]` + "\n" + + "---END example1---\n\n" + + "---BEGIN example2---\n" + + `[{"beginRendering": {"surfaceId": "id"}}]` + "\n" + + "---END example2---", + }, + { + name: "glob prefix", + path: root + "/glob_filter/user_*.json", + want: "---BEGIN user_profile---\n" + + `[{"beginRendering": {"surfaceId": "user"}}]` + "\n" + + "---END user_profile---\n\n" + + "---BEGIN user_settings---\n" + + `[{"beginRendering": {"surfaceId": "settings"}}]` + "\n" + + "---END user_settings---", + }, + { + name: "glob range", + path: root + "/glob_range/step[1-2].json", + want: "---BEGIN step1---\n" + + `[{"beginRendering": {"surfaceId": "1"}}]` + "\n" + + "---END step1---\n\n" + + "---BEGIN step2---\n" + + `[{"beginRendering": {"surfaceId": "2"}}]` + "\n" + + "---END step2---", + }, + { + name: "glob negation", + path: root + "/glob_negation/[!i]*.json", + want: "---BEGIN visible---\n" + + `[{"beginRendering": {"surfaceId": "visible"}}]` + "\n" + + "---END visible---", + }, + { + name: "recursive glob", + path: root + "/glob_recursive/**/*.json", + want: "---BEGIN deep---\n" + + `[{"beginRendering": {"surfaceId": "deep"}}]` + "\n" + + "---END deep---\n\n" + + "---BEGIN top---\n" + + `[{"beginRendering": {"surfaceId": "top"}}]` + "\n" + + "---END top---", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := catalog.LoadExamples(tt.path, false) + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Fatalf("LoadExamples() = %q, want %q", got, tt.want) + } + }) + } +} + +func makeLoadExamplesFixtures(t *testing.T) string { + t.Helper() + root := t.TempDir() + files := map[string]string{ + "basic/example1.json": `[{"beginRendering": {"surfaceId": "id"}}]`, + "basic/example2.json": `[{"beginRendering": {"surfaceId": "id"}}]`, + "basic/ignored.txt": "ignored", + "glob_filter/admin_profile.json": `[{"beginRendering": {"surfaceId": "admin"}}]`, + "glob_filter/user_profile.json": `[{"beginRendering": {"surfaceId": "user"}}]`, + "glob_filter/user_settings.json": `[{"beginRendering": {"surfaceId": "settings"}}]`, + "glob_negation/index.json": `[{"beginRendering": {"surfaceId": "index"}}]`, + "glob_negation/visible.json": `[{"beginRendering": {"surfaceId": "visible"}}]`, + "glob_range/step1.json": `[{"beginRendering": {"surfaceId": "1"}}]`, + "glob_range/step2.json": `[{"beginRendering": {"surfaceId": "2"}}]`, + "glob_range/step3.json": `[{"beginRendering": {"surfaceId": "3"}}]`, + "glob_recursive/ignored.txt": "ignored", + "glob_recursive/nested/deep.json": `[{"beginRendering": {"surfaceId": "deep"}}]`, + "glob_recursive/top.json": `[{"beginRendering": {"surfaceId": "top"}}]`, + } + for name, content := range files { + path := filepath.Join(root, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o666); err != nil { + t.Fatal(err) + } + } + return root +} diff --git a/agent_sdks/go/a2uischema/constants.go b/agent_sdks/go/a2uischema/constants.go new file mode 100644 index 0000000000..beb79a279a --- /dev/null +++ b/agent_sdks/go/a2uischema/constants.go @@ -0,0 +1,24 @@ +package a2uischema + +const ( + SupportedCatalogIDsKey = "supportedCatalogIds" + InlineCatalogsKey = "inlineCatalogs" + CatalogComponentsKey = "components" + CatalogFunctionsKey = "functions" + CatalogIDKey = "catalogId" + CatalogThemeKey = "theme" + InlineCatalogName = "inline" + A2UIOpenTag = "" + A2UICloseTag = "" + A2UISchemaBlockStart = "---BEGIN A2UI JSON SCHEMA---" + A2UISchemaBlockEnd = "---END A2UI JSON SCHEMA---" + DefaultWorkflowRules = "The generated response MUST follow these rules:\n- The response can contain one or more A2UI JSON blocks.\n- Each A2UI JSON block MUST be wrapped in `` and `` tags.\n- Between or around these blocks, you can provide conversational text.\n- The JSON part MUST be a single, raw JSON object or array of A2UI messages and MUST validate against the provided A2UI JSON SCHEMA.\n- Component IDs referenced by a component MUST be defined in the same update or already exist on the surface." +) + +type Version string + +const ( + Version09 Version = "v0.9" + Version091 Version = "v0.9.1" + Version010 Version = "v0.10" +) diff --git a/agent_sdks/go/a2uischema/doc.go b/agent_sdks/go/a2uischema/doc.go new file mode 100644 index 0000000000..16d6873403 --- /dev/null +++ b/agent_sdks/go/a2uischema/doc.go @@ -0,0 +1,2 @@ +// Package a2uischema provides JSON schema validation for A2UI messages. +package a2uischema diff --git a/agent_sdks/go/a2uischema/error.go b/agent_sdks/go/a2uischema/error.go new file mode 100644 index 0000000000..17c8baf246 --- /dev/null +++ b/agent_sdks/go/a2uischema/error.go @@ -0,0 +1,44 @@ +package a2uischema + +// ValidationCode identifies a class of validation failure. +type ValidationCode string + +const ( + ValidationDuplicateComponent ValidationCode = "duplicate_component" + ValidationUnknownComponentRef ValidationCode = "unknown_component_ref" + ValidationMissingRootComponent ValidationCode = "missing_root_component" + ValidationCycle ValidationCode = "cycle" + ValidationOrphanedComponent ValidationCode = "orphaned_component" + ValidationUnknownComponentType ValidationCode = "unknown_component_type" + ValidationUnknownFunction ValidationCode = "unknown_function" + ValidationInvalidPath ValidationCode = "invalid_path" +) + +// ValidationError describes a validation failure in a form callers can inspect. +type ValidationError struct { + Code ValidationCode + Path string + Component string + Ref string + Function string + Message string +} + +// Error returns the human-readable validation message. +func (e *ValidationError) Error() string { + if e == nil { + return "" + } + return e.Message +} + +func validationError(code ValidationCode, path, component, ref, function, message string) error { + return &ValidationError{ + Code: code, + Path: path, + Component: component, + Ref: ref, + Function: function, + Message: message, + } +} diff --git a/agent_sdks/go/a2uischema/manager.go b/agent_sdks/go/a2uischema/manager.go new file mode 100644 index 0000000000..b16841350b --- /dev/null +++ b/agent_sdks/go/a2uischema/manager.go @@ -0,0 +1,557 @@ +package a2uischema + +import ( + "encoding/json" + "fmt" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + a2uiv010 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010" + a2uiv091 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v091" +) + +// SchemaModifier can rewrite a decoded schema before it is used. +type SchemaModifier func(schema map[string]any) error + +// SchemaManager manages schemas, catalogs, and prompt rendering. +type SchemaManager struct { + version Version + acceptsInlineCatalogs bool + serverToClientSchema map[string]any + commonTypesSchema map[string]any + supportedCatalogs []*Catalog + catalogExamplePaths map[string]string + schemaModifiers []SchemaModifier +} + +// NewSchemaManager constructs a schema manager. +func NewSchemaManager(version Version, catalogs []CatalogConfig, acceptsInlineCatalogs bool, schemaModifiers ...SchemaModifier) (*SchemaManager, error) { + serverSchema, commonSchema, err := embeddedSchemas(version) + if err != nil { + return nil, err + } + manager := &SchemaManager{ + version: version, + acceptsInlineCatalogs: acceptsInlineCatalogs, + serverToClientSchema: serverSchema, + commonTypesSchema: commonSchema, + catalogExamplePaths: make(map[string]string), + schemaModifiers: schemaModifiers, + } + for _, cfg := range catalogs { + data, err := cfg.Provider.Load() + if err != nil { + return nil, fmt.Errorf("schema: load catalog %q: %w", cfg.Name, err) + } + serverSchemaData, err := marshalJSON(serverSchema) + if err != nil { + return nil, fmt.Errorf("schema: encode server_to_client schema: %w", err) + } + commonSchemaData, err := marshalJSON(commonSchema) + if err != nil { + return nil, fmt.Errorf("schema: encode common_types schema: %w", err) + } + catalog, err := newCatalog(version, cfg.Name, serverSchemaData, commonSchemaData, data) + if err != nil { + return nil, err + } + if err := manager.applyModifiers(catalog.ServerToClientSchema); err != nil { + return nil, err + } + if err := manager.applyModifiers(catalog.CommonTypesSchema); err != nil { + return nil, err + } + if err := manager.applyModifiers(catalog.CatalogSchema); err != nil { + return nil, err + } + manager.supportedCatalogs = append(manager.supportedCatalogs, catalog) + if cfg.ExamplesPath != "" { + id, err := catalog.ID() + if err != nil { + return nil, err + } + manager.catalogExamplePaths[id] = cfg.ExamplesPath + } + } + return manager, nil +} + +// SupportedCatalogIDs returns the agent-supported catalog identifiers. +func (m *SchemaManager) SupportedCatalogIDs() []string { + ids := make([]string, 0, len(m.supportedCatalogs)) + for _, catalog := range m.supportedCatalogs { + id, err := catalog.ID() + if err == nil { + ids = append(ids, id) + } + } + return ids +} + +// SelectedCatalog selects the catalog for the provided client capabilities and pruning. +func (m *SchemaManager) SelectedCatalog(clientCapabilities any, allowedComponents, allowedMessages []string) (*Catalog, error) { + selected, err := m.selectCatalogFor(clientCapabilities) + if err != nil { + return nil, err + } + return selected.WithPruning(allowedComponents, allowedMessages) +} + +// LoadExamples loads examples for a catalog if configured. +func (m *SchemaManager) LoadExamples(catalog *Catalog, validate bool) (string, error) { + if catalog == nil { + return "", fmt.Errorf("schema: nil catalog") + } + id, err := catalog.ID() + if err != nil { + return "", err + } + path := m.catalogExamplePaths[id] + return catalog.LoadExamples(path, validate) +} + +// GenerateSystemPrompt assembles the system prompt. +func (m *SchemaManager) GenerateSystemPrompt(roleDescription, workflowDescription, uiDescription string, clientCapabilities any, allowedComponents, allowedMessages []string, includeSchema, includeExamples, validateExamples bool) (string, error) { + catalog, err := m.SelectedCatalog(clientCapabilities, allowedComponents, allowedMessages) + if err != nil { + return "", err + } + return m.generateSystemPrompt(catalog, roleDescription, workflowDescription, uiDescription, includeSchema, includeExamples, validateExamples) +} + +func (m *SchemaManager) generateSystemPrompt(catalog *Catalog, roleDescription, workflowDescription, uiDescription string, includeSchema, includeExamples, validateExamples bool) (string, error) { + parts := []string{roleDescription} + workflow := DefaultWorkflowRules + if workflowDescription != "" { + workflow += "\n" + workflowDescription + } + parts = append(parts, "## Workflow Description:\n"+workflow) + if uiDescription != "" { + parts = append(parts, "## UI Description:\n"+uiDescription) + } + if includeSchema { + schemaBlock, err := catalog.RenderAsLLMInstructions() + if err != nil { + return "", err + } + parts = append(parts, schemaBlock) + } + if includeExamples { + examples, err := m.LoadExamples(catalog, validateExamples) + if err != nil { + return "", err + } + if examples != "" { + parts = append(parts, "### Examples:\n"+examples) + } + } + return joinPromptParts(parts), nil +} + +func (m *SchemaManager) applyModifiers(schema map[string]any) error { + for _, modifier := range m.schemaModifiers { + if err := modifier(schema); err != nil { + return err + } + } + return nil +} + +func (m *SchemaManager) selectCatalog(clientCapabilities *a2ui.ClientCapabilities) (*Catalog, error) { + if len(m.supportedCatalogs) == 0 { + return nil, fmt.Errorf("schema: no supported catalogs configured") + } + if clientCapabilities == nil || clientCapabilities.V09 == nil { + return m.supportedCatalogs[0], nil + } + caps := clientCapabilities.V09 + if len(caps.InlineCatalogs) > 0 { + if !m.acceptsInlineCatalogs { + return nil, fmt.Errorf("schema: inline catalogs provided but not accepted") + } + base := m.supportedCatalogs[0] + if len(caps.SupportedCatalogIDs) > 0 { + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + base = catalog + break + } + } + } + } + return mergeInlineCatalogs(m.version, base, caps.InlineCatalogs) + } + if len(caps.SupportedCatalogIDs) == 0 { + return m.supportedCatalogs[0], nil + } + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + return catalog, nil + } + } + } + return nil, fmt.Errorf("schema: no mutually supported catalog found") +} + +func (m *SchemaManager) selectCatalogV010(clientCapabilities *a2uiv010.ClientCapabilities) (*Catalog, error) { + if m.version != Version010 { + return nil, fmt.Errorf("schema: manager version = %q, want %q", m.version, Version010) + } + if len(m.supportedCatalogs) == 0 { + return nil, fmt.Errorf("schema: no supported catalogs configured") + } + if clientCapabilities == nil || clientCapabilities.V010 == nil { + return m.supportedCatalogs[0], nil + } + caps := clientCapabilities.V010 + if len(caps.InlineCatalogs) > 0 { + if !m.acceptsInlineCatalogs { + return nil, fmt.Errorf("schema: inline catalogs provided but not accepted") + } + base := m.supportedCatalogs[0] + if len(caps.SupportedCatalogIDs) > 0 { + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + base = catalog + break + } + } + } + } + return mergeInlineCatalogsV010(m.version, base, caps.InlineCatalogs) + } + if len(caps.SupportedCatalogIDs) == 0 { + return m.supportedCatalogs[0], nil + } + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + return catalog, nil + } + } + } + return nil, fmt.Errorf("schema: no mutually supported catalog found") +} + +func (m *SchemaManager) selectCatalogFor(clientCapabilities any) (*Catalog, error) { + switch caps := clientCapabilities.(type) { + case nil: + if m.version == Version010 { + return m.selectCatalogV010(nil) + } + return m.selectCatalog(nil) + case *a2ui.ClientCapabilities: + if !isV09WireVersion(m.version) { + return nil, fmt.Errorf("schema: manager version = %q, got v0.9 capabilities", m.version) + } + return m.selectCatalog(caps) + case *a2uiv091.ClientCapabilities: + if m.version != Version091 { + return nil, fmt.Errorf("schema: manager version = %q, got v0.9.1 capabilities", m.version) + } + return m.selectCatalogV091(caps) + case *a2uiv010.ClientCapabilities: + if m.version != Version010 { + return nil, fmt.Errorf("schema: manager version = %q, got v0.10 capabilities", m.version) + } + return m.selectCatalogV010(caps) + default: + return nil, fmt.Errorf("schema: unsupported client capabilities type %T", clientCapabilities) + } +} + +func (m *SchemaManager) selectCatalogV091(clientCapabilities *a2uiv091.ClientCapabilities) (*Catalog, error) { + if m.version != Version091 { + return nil, fmt.Errorf("schema: manager version = %q, want %q", m.version, Version091) + } + if len(m.supportedCatalogs) == 0 { + return nil, fmt.Errorf("schema: no supported catalogs configured") + } + if clientCapabilities == nil || clientCapabilities.V091 == nil { + return m.supportedCatalogs[0], nil + } + caps := clientCapabilities.V091 + if len(caps.InlineCatalogs) > 0 { + if !m.acceptsInlineCatalogs { + return nil, fmt.Errorf("schema: inline catalogs provided but not accepted") + } + base := m.supportedCatalogs[0] + if len(caps.SupportedCatalogIDs) > 0 { + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + base = catalog + break + } + } + } + } + return mergeInlineCatalogsV091(m.version, base, caps.InlineCatalogs) + } + if len(caps.SupportedCatalogIDs) == 0 { + return m.supportedCatalogs[0], nil + } + for _, id := range caps.SupportedCatalogIDs { + for _, catalog := range m.supportedCatalogs { + catalogID, err := catalog.ID() + if err == nil && catalogID == id { + return catalog, nil + } + } + } + return nil, fmt.Errorf("schema: no mutually supported catalog found") +} + +func mergeInlineCatalogs(version Version, base *Catalog, inlineCatalogs []a2ui.CatalogDef) (*Catalog, error) { + serverSchema, commonSchema, catalogSchema, err := cloneCatalogSchemas(base) + if err != nil { + return nil, err + } + merged := &Catalog{ + Version: version, + Name: InlineCatalogName, + ServerToClientSchema: serverSchema, + CommonTypesSchema: commonSchema, + CatalogSchema: catalogSchema, + } + for _, inline := range inlineCatalogs { + if inline.CatalogID != "" { + merged.CatalogSchema[CatalogIDKey] = inline.CatalogID + } + components, _ := merged.CatalogSchema[CatalogComponentsKey].(map[string]any) + if components == nil { + components = make(map[string]any) + merged.CatalogSchema[CatalogComponentsKey] = components + } + for name, raw := range inline.Components { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline component %q: %w", name, err) + } + components[name] = decoded + } + if len(inline.Theme) > 0 { + theme, _ := merged.CatalogSchema[CatalogThemeKey].(map[string]any) + if theme == nil { + theme = make(map[string]any) + merged.CatalogSchema[CatalogThemeKey] = theme + } + for name, raw := range inline.Theme { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline theme %q: %w", name, err) + } + theme[name] = decoded + } + } + if err := mergeInlineFunctions(merged.CatalogSchema, inline.Functions); err != nil { + return nil, err + } + } + return merged, nil +} + +func mergeInlineCatalogsV091(version Version, base *Catalog, inlineCatalogs []a2uiv091.CatalogDef) (*Catalog, error) { + serverSchema, commonSchema, catalogSchema, err := cloneCatalogSchemas(base) + if err != nil { + return nil, err + } + merged := &Catalog{ + Version: version, + Name: InlineCatalogName, + ServerToClientSchema: serverSchema, + CommonTypesSchema: commonSchema, + CatalogSchema: catalogSchema, + } + for _, inline := range inlineCatalogs { + if inline.CatalogID != "" { + merged.CatalogSchema[CatalogIDKey] = inline.CatalogID + } + components, _ := merged.CatalogSchema[CatalogComponentsKey].(map[string]any) + if components == nil { + components = make(map[string]any) + merged.CatalogSchema[CatalogComponentsKey] = components + } + for name, raw := range inline.Components { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline component %q: %w", name, err) + } + components[name] = decoded + } + if len(inline.Theme) > 0 { + theme, _ := merged.CatalogSchema[CatalogThemeKey].(map[string]any) + if theme == nil { + theme = make(map[string]any) + merged.CatalogSchema[CatalogThemeKey] = theme + } + for name, raw := range inline.Theme { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline theme %q: %w", name, err) + } + theme[name] = decoded + } + } + if err := mergeInlineFunctions(merged.CatalogSchema, inline.Functions); err != nil { + return nil, err + } + } + return merged, nil +} + +func mergeInlineCatalogsV010(version Version, base *Catalog, inlineCatalogs []a2uiv010.CatalogDef) (*Catalog, error) { + serverSchema, commonSchema, catalogSchema, err := cloneCatalogSchemas(base) + if err != nil { + return nil, err + } + merged := &Catalog{ + Version: version, + Name: InlineCatalogName, + ServerToClientSchema: serverSchema, + CommonTypesSchema: commonSchema, + CatalogSchema: catalogSchema, + } + for _, inline := range inlineCatalogs { + if inline.CatalogID != "" { + merged.CatalogSchema[CatalogIDKey] = inline.CatalogID + } + components, _ := merged.CatalogSchema[CatalogComponentsKey].(map[string]any) + if components == nil { + components = make(map[string]any) + merged.CatalogSchema[CatalogComponentsKey] = components + } + for name, raw := range inline.Components { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline component %q: %w", name, err) + } + components[name] = decoded + } + if len(inline.Theme) > 0 { + theme, _ := merged.CatalogSchema[CatalogThemeKey].(map[string]any) + if theme == nil { + theme = make(map[string]any) + merged.CatalogSchema[CatalogThemeKey] = theme + } + for name, raw := range inline.Theme { + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil, fmt.Errorf("schema: decode inline theme %q: %w", name, err) + } + theme[name] = decoded + } + } + if err := mergeInlineFunctions(merged.CatalogSchema, inline.Functions); err != nil { + return nil, err + } + } + return merged, nil +} + +func mergeInlineFunctions(catalogSchema map[string]any, functions any) error { + data, err := json.Marshal(functions) + if err != nil { + return fmt.Errorf("schema: encode inline functions: %w", err) + } + var defs []map[string]any + if err := json.Unmarshal(data, &defs); err != nil { + return fmt.Errorf("schema: decode inline functions: %w", err) + } + if len(defs) == 0 { + return nil + } + functionsMap, _ := catalogSchema[CatalogFunctionsKey].(map[string]any) + if functionsMap != nil { + for _, def := range defs { + name, _ := def["name"].(string) + if name == "" { + return fmt.Errorf("schema: inline function missing name") + } + functionsMap[name] = def + } + return nil + } + functionsList, _ := catalogSchema[CatalogFunctionsKey].([]any) + for _, def := range defs { + functionsList = append(functionsList, def) + } + catalogSchema[CatalogFunctionsKey] = functionsList + return nil +} + +func embeddedSchemas(version Version) (map[string]any, map[string]any, error) { + switch version { + case Version09: + serverMap, err := unmarshalJSONMap(serverToClientV09) + if err != nil { + return nil, nil, err + } + commonMap, err := unmarshalJSONMap(commonTypesV09) + if err != nil { + return nil, nil, err + } + return serverMap, commonMap, nil + case Version091: + serverMap, err := unmarshalJSONMap(serverToClientV091) + if err != nil { + return nil, nil, err + } + commonMap, err := unmarshalJSONMap(commonTypesV091) + if err != nil { + return nil, nil, err + } + return serverMap, commonMap, nil + case Version010: + serverMap, err := unmarshalJSONMap(serverToClientV010) + if err != nil { + return nil, nil, err + } + commonMap, err := unmarshalJSONMap(commonTypesV010) + if err != nil { + return nil, nil, err + } + return serverMap, commonMap, nil + default: + return nil, nil, fmt.Errorf("schema: unsupported version %q", version) + } +} + +func marshalJSON(v any) ([]byte, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + return data, nil +} + +func joinPromptParts(parts []string) string { + out := make([]string, 0, len(parts)) + for _, part := range parts { + if part != "" { + out = append(out, part) + } + } + return string(bytesJoinWithDoubleNewline(out)) +} + +func bytesJoinWithDoubleNewline(parts []string) []byte { + if len(parts) == 0 { + return nil + } + var out []byte + for i, part := range parts { + if i > 0 { + out = append(out, '\n', '\n') + } + out = append(out, part...) + } + return out +} diff --git a/agent_sdks/go/a2uischema/provider.go b/agent_sdks/go/a2uischema/provider.go new file mode 100644 index 0000000000..4a3f7efd08 --- /dev/null +++ b/agent_sdks/go/a2uischema/provider.go @@ -0,0 +1,48 @@ +package a2uischema + +import ( + "fmt" + "os" +) + +// CatalogProvider loads a catalog schema. +type CatalogProvider interface { + Load() ([]byte, error) +} + +// StaticCatalogProvider returns a fixed schema payload. +type StaticCatalogProvider struct { + Data []byte +} + +// Load implements [CatalogProvider]. +func (p StaticCatalogProvider) Load() ([]byte, error) { + if len(p.Data) == 0 { + return nil, fmt.Errorf("schema: static catalog provider has no data") + } + data := make([]byte, len(p.Data)) + copy(data, p.Data) + return data, nil +} + +// FileSystemCatalogProvider loads a catalog schema from disk. +type FileSystemCatalogProvider string + +// Load implements [CatalogProvider]. +func (p FileSystemCatalogProvider) Load() ([]byte, error) { + return os.ReadFile(string(p)) +} + +// BasicCatalogProvider returns the embedded basic catalog provider for a version. +func BasicCatalogProvider(version Version) (CatalogProvider, error) { + switch version { + case Version09: + return StaticCatalogProvider{Data: basicCatalogV09}, nil + case Version091: + return StaticCatalogProvider{Data: basicCatalogV091}, nil + case Version010: + return StaticCatalogProvider{Data: basicCatalogV010}, nil + default: + return nil, fmt.Errorf("schema: unsupported version %q", version) + } +} diff --git a/agent_sdks/go/a2uischema/schema_test.go b/agent_sdks/go/a2uischema/schema_test.go new file mode 100644 index 0000000000..122c42f8f7 --- /dev/null +++ b/agent_sdks/go/a2uischema/schema_test.go @@ -0,0 +1,309 @@ +package a2uischema + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + a2uiv010 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010" + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uibuild" + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uistream" +) + +func TestSchemaManagerGenerateSystemPrompt(t *testing.T) { + basic, err := BasicCatalogConfig(Version09) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version09, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + prompt, err := manager.GenerateSystemPrompt("role", "", "", nil, nil, nil, true, false, false) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(prompt, A2UISchemaBlockStart) { + t.Fatal("expected schema block") + } + if !strings.Contains(prompt, "catalogs/basic/catalog.json") { + t.Fatal("expected basic catalog schema in prompt") + } +} + +func TestSchemaManagerGenerateSystemPromptVersioned(t *testing.T) { + basic, err := BasicCatalogConfig(Version010) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version010, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + caps := &a2uiv010.ClientCapabilities{V010: &a2uiv010.ClientCapabilitiesV010{ + SupportedCatalogIDs: []string{"https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json"}, + }} + prompt, err := manager.GenerateSystemPrompt("role", "", "", caps, nil, nil, true, false, false) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(prompt, A2UISchemaBlockStart) { + t.Fatal("expected schema block") + } + if !strings.Contains(prompt, "v0_10/catalogs/basic/catalog.json") { + t.Fatal("expected v0.10 basic catalog schema in prompt") + } +} + +func TestSchemaManagerGenerateSystemPromptV091(t *testing.T) { + basic, err := BasicCatalogConfig(Version091) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version091, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + prompt, err := manager.GenerateSystemPrompt("role", "", "", nil, nil, nil, true, false, false) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(prompt, A2UISchemaBlockStart) { + t.Fatal("expected schema block") + } + if !strings.Contains(prompt, "v0_9/catalogs/basic/catalog.json") { + t.Fatal("expected v0.9 wire catalog schema in prompt") + } +} + +func TestValidatorAcceptsV091WireVersion(t *testing.T) { + validator := mustBasicValidatorV091(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "s1", + CatalogID: "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + }, + } + if err := validator.ValidateMessages([]a2ui.ServerMessage{msg}); err != nil { + t.Fatal(err) + } +} + +func TestValidatorAcceptsValidSurfaceMessages(t *testing.T) { + validator := mustBasicValidator(t) + surface := a2uibuild.NewSurface("contact", "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"). + Add(a2uibuild.Column("root", a2uibuild.Children("greeting"))). + Add(a2uibuild.Text("greeting", a2ui.StringLiteral("Hello, world!"))) + if err := validator.ValidateMessages(surface.Messages()); err != nil { + t.Fatal(err) + } +} + +func TestValidatorAcceptsV010Examples(t *testing.T) { + validator := mustBasicValidatorV010(t) + paths, err := filepath.Glob("testdata/v0_10/basic/examples/*.json") + if err != nil { + t.Fatal(err) + } + if len(paths) == 0 { + t.Fatal("no v0.10 examples found") + } + for _, path := range paths { + t.Run(filepath.Base(path), func(t *testing.T) { + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := validator.ValidateExample(data); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestValidatorAcceptsV010ActionResponseNull(t *testing.T) { + validator := mustBasicValidatorV010(t) + msg := a2uiv010.ServerMessage{ + Version: a2uiv010.Version, + ActionID: "action-1", + ActionResponse: ptr(a2uiv010.ActionResponseValue(nil)), + } + if err := validator.ValidateVersionMessages([]a2uiv010.ServerMessage{msg}); err != nil { + t.Fatal(err) + } +} + +func TestValidatorRejectsDuplicateIDs(t *testing.T) { + validator := mustBasicValidator(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: "s1", + Components: []a2ui.Component{ + a2uibuild.Column("root", a2uibuild.Children("dup")), + a2uibuild.Text("dup", a2ui.StringLiteral("one")), + a2uibuild.Text("dup", a2ui.StringLiteral("two")), + }, + }, + } + err := validator.ValidateMessages([]a2ui.ServerMessage{msg}) + if err == nil { + t.Fatal("expected validation error, got nil") + } + assertValidationError(t, err, ValidationDuplicateComponent, "dup") +} + +func TestValidatorRejectsOrphanedComponent(t *testing.T) { + validator := mustBasicValidator(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: "s1", + Components: []a2ui.Component{ + a2uibuild.Column("root", a2uibuild.Children("greeting")), + a2uibuild.Text("greeting", a2ui.StringLiteral("hello")), + a2uibuild.Text("extra", a2ui.StringLiteral("orphan")), + }, + }, + } + err := validator.ValidateMessages([]a2ui.ServerMessage{msg}) + if err == nil { + t.Fatal("expected validation error, got nil") + } + assertValidationError(t, err, ValidationOrphanedComponent, "") +} + +func TestValidatorRejectsUnknownFunction(t *testing.T) { + validator := mustBasicValidator(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: "s1", + Components: []a2ui.Component{ + a2uibuild.Button("root", + a2ui.Action{ + FunctionCall: &a2ui.FunctionCall{Call: "definitelyUnknown"}, + }, + "label", + ), + a2uibuild.Text("label", a2ui.StringLiteral("Run")), + }, + }, + } + err := validator.ValidateMessages([]a2ui.ServerMessage{msg}) + if err == nil { + t.Fatal("expected validation error, got nil") + } + assertValidationError(t, err, ValidationUnknownFunction, "") +} + +func TestValidatorReportsStructuredInvalidPath(t *testing.T) { + validator := mustBasicValidator(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateDataModel: &a2ui.UpdateDataModel{ + SurfaceID: "s1", + Path: "/bad~path", + Value: "value", + }, + } + err := validator.ValidateMessages([]a2ui.ServerMessage{msg}) + if err == nil { + t.Fatal("expected validation error, got nil") + } + assertValidationError(t, err, ValidationInvalidPath, "") +} + +func TestParseAndValidate(t *testing.T) { + validator := mustBasicValidator(t) + msg := a2ui.ServerMessage{ + Version: a2ui.Version, + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: "s1", + Components: []a2ui.Component{ + a2uibuild.Text("bad", a2ui.StringLiteral("missing root")), + }, + }, + } + data, err := json.Marshal(msg) + if err != nil { + t.Fatal(err) + } + if _, err := a2uistream.ParseAndValidate(string(data), validator); err == nil { + t.Fatal("expected validation error, got nil") + } +} + +func mustBasicValidator(t *testing.T) *Validator { + t.Helper() + basic, err := BasicCatalogConfig(Version09) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version09, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + catalog, err := manager.SelectedCatalog(nil, nil, nil) + if err != nil { + t.Fatal(err) + } + return catalog.Validator() +} + +func mustBasicValidatorV010(t *testing.T) *Validator { + t.Helper() + basic, err := BasicCatalogConfig(Version010) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version010, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + catalog, err := manager.SelectedCatalog(nil, nil, nil) + if err != nil { + t.Fatal(err) + } + return catalog.Validator() +} + +func mustBasicValidatorV091(t *testing.T) *Validator { + t.Helper() + basic, err := BasicCatalogConfig(Version091) + if err != nil { + t.Fatal(err) + } + manager, err := NewSchemaManager(Version091, []CatalogConfig{basic}, false) + if err != nil { + t.Fatal(err) + } + catalog, err := manager.SelectedCatalog(nil, nil, nil) + if err != nil { + t.Fatal(err) + } + return catalog.Validator() +} + +func ptr[T any](v T) *T { + return &v +} + +func assertValidationError(t *testing.T, err error, code ValidationCode, component string) { + t.Helper() + var validationErr *ValidationError + if !errors.As(err, &validationErr) { + t.Fatalf("errors.As(*ValidationError) = false for %v", err) + } + if validationErr.Code != code { + t.Fatalf("ValidationError.Code = %q, want %q", validationErr.Code, code) + } + if component != "" && validationErr.Component != component { + t.Fatalf("ValidationError.Component = %q, want %q", validationErr.Component, component) + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog.json b/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog.json new file mode 100644 index 0000000000..1f1204c513 --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog.json @@ -0,0 +1,1189 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "title": "A2UI Basic Catalog", + "description": "Unified catalog of basic A2UI components and functions.", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Text"}, + "text": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"], + "default": "body" + } + }, + "required": ["component", "text"] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Image"}, + "url": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "description": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "Accessibility text for the image." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": ["contain", "cover", "fill", "none", "scaleDown"], + "default": "fill" + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"], + "default": "mediumFeature" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Icon"}, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "path": {"type": "string"} + }, + "required": ["path"], + "additionalProperties": false + } + ] + } + }, + "required": ["component", "name"] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Video"}, + "url": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + }, + "posterUrl": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The URL of the poster image to display before the video plays." + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "AudioPlayer"}, + "url": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": {"const": "Row"}, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": {"const": "Column"}, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": ["center", "end", "start", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "List"}, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": ["vertical", "horizontal"], + "default": "vertical" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Card"}, + "child": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": ["component", "child"] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Tabs"}, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": ["title", "child"], + "additionalProperties": false + } + } + }, + "required": ["component", "tabs"] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Modal"}, + "trigger": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": ["component", "trigger", "content"] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Divider"}, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": ["horizontal", "vertical"], + "default": "horizontal" + } + }, + "required": ["component"] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "properties": { + "component": {"const": "Button"}, + "child": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": ["default", "primary", "borderless"], + "default": "default" + }, + "action": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Action" + } + }, + "required": ["component", "child", "action"] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "properties": { + "component": {"const": "TextField"}, + "label": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "placeholder": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The placeholder text for the input field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": ["longText", "number", "shortText", "obscured"], + "default": "shortText" + } + }, + "required": ["component", "label"] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "properties": { + "component": {"const": "CheckBox"}, + "label": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": ["component", "label", "value"] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": {"const": "ChoicePicker"}, + "label": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": ["multipleSelection", "mutuallyExclusive"], + "default": "mutuallyExclusive" + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + }, + "displayStyle": { + "type": "string", + "description": "The display style of the component.", + "enum": ["checkbox", "chips"], + "default": "checkbox" + }, + "filterable": { + "type": "boolean", + "description": "If true, displays a search input to filter the options.", + "default": false + } + }, + "required": ["component", "options", "value"] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "properties": { + "component": {"const": "Slider"}, + "label": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider.", + "default": 0 + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + }, + "steps": { + "type": "integer", + "minimum": 1, + "description": "The number of discrete divisions in the slider range. If specified, the slider will snap to discrete values." + } + }, + "required": ["component", "value", "max"] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + {"$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/Checkable"}, + { + "type": "object", + "properties": { + "component": {"const": "DateTimeInput"}, + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date.", + "default": false + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time.", + "default": false + }, + "min": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The maximum allowed date/time in ISO 8601 format." + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": ["component", "value"] + } + ], + "unevaluatedProperties": false + } + }, + "functions": { + "required": { + "type": "object", + "description": "Checks that the value is not null, undefined, or empty.", + "properties": { + "call": { + "const": "required" + }, + "args": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], + "additionalProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "regex": { + "type": "object", + "description": "Checks that the value matches a regular expression string.", + "properties": { + "call": { + "const": "regex" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } + }, + "required": ["value", "pattern"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "length": { + "type": "object", + "description": "Checks string length constraints.", + "properties": { + "call": { + "const": "length" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." + } + }, + "required": ["value"], + "anyOf": [{"required": ["min"]}, {"required": ["max"]}], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "numeric": { + "type": "object", + "description": "Checks numeric range constraints.", + "properties": { + "call": { + "const": "numeric" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber" + }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." + } + }, + "required": ["value"], + "anyOf": [{"required": ["min"]}, {"required": ["max"]}], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "email": { + "type": "object", + "description": "Checks that the value is a valid email address.", + "properties": { + "call": { + "const": "email" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatString": { + "type": "object", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", + "properties": { + "call": { + "const": "formatString" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatNumber": { + "type": "object", + "description": "Formats a number with the specified grouping and decimal precision.", + "properties": { + "call": { + "const": "formatNumber" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatCurrency": { + "type": "object", + "description": "Formats a number as a currency string.", + "properties": { + "call": { + "const": "formatCurrency" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["currency", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatDate": { + "type": "object", + "description": "Formats a timestamp into a string using a pattern.", + "properties": { + "call": { + "const": "formatDate" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + } + }, + "required": ["format", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "pluralize": { + "type": "object", + "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", + "properties": { + "call": { + "const": "pluralize" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." + }, + "zero": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." + } + }, + "required": ["value", "other"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "openUrl": { + "type": "object", + "description": "Opens the specified URL in a browser or handler. This function has no return value.", + "properties": { + "call": { + "const": "openUrl" + }, + "args": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The URL to open." + } + }, + "required": ["url"], + "additionalProperties": false + }, + "returnType": { + "const": "void" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "and": { + "type": "object", + "description": "Performs a logical AND operation on a list of boolean values.", + "properties": { + "call": { + "const": "and" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "or": { + "type": "object", + "description": "Performs a logical OR operation on a list of boolean values.", + "properties": { + "call": { + "const": "or" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "not": { + "type": "object", + "description": "Performs a logical NOT operation on a boolean value.", + "properties": { + "call": { + "const": "not" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_10/common_types.json#/$defs/DynamicBoolean", + "description": "The boolean value to negate." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." + } + } + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + {"$ref": "#/components/Text"}, + {"$ref": "#/components/Image"}, + {"$ref": "#/components/Icon"}, + {"$ref": "#/components/Video"}, + {"$ref": "#/components/AudioPlayer"}, + {"$ref": "#/components/Row"}, + {"$ref": "#/components/Column"}, + {"$ref": "#/components/List"}, + {"$ref": "#/components/Card"}, + {"$ref": "#/components/Tabs"}, + {"$ref": "#/components/Modal"}, + {"$ref": "#/components/Divider"}, + {"$ref": "#/components/Button"}, + {"$ref": "#/components/TextField"}, + {"$ref": "#/components/CheckBox"}, + {"$ref": "#/components/ChoicePicker"}, + {"$ref": "#/components/Slider"}, + {"$ref": "#/components/DateTimeInput"} + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "oneOf": [ + {"$ref": "#/functions/required"}, + {"$ref": "#/functions/regex"}, + {"$ref": "#/functions/length"}, + {"$ref": "#/functions/numeric"}, + {"$ref": "#/functions/email"}, + {"$ref": "#/functions/formatString"}, + {"$ref": "#/functions/formatNumber"}, + {"$ref": "#/functions/formatCurrency"}, + {"$ref": "#/functions/formatDate"}, + {"$ref": "#/functions/pluralize"}, + {"$ref": "#/functions/openUrl"}, + {"$ref": "#/functions/and"}, + {"$ref": "#/functions/or"}, + {"$ref": "#/functions/not"} + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog_rules.txt b/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog_rules.txt new file mode 100644 index 0000000000..33e601348e --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_10/basic_catalog_rules.txt @@ -0,0 +1,5 @@ +**REQUIRED PROPERTIES:** You MUST include ALL required properties for every component, even if they are inside a template or will be bound to data. +- For 'Text', you MUST provide 'text'. If dynamic, use { "path": "..." }. +- For 'Image', you MUST provide 'url'. If dynamic, use { "path": "..." }. +- For 'Button', you MUST provide 'action'. +- For 'TextField', 'CheckBox', etc., you MUST provide 'label'. diff --git a/agent_sdks/go/a2uischema/schemas/v0_10/common_types.json b/agent_sdks/go/a2uischema/schemas/v0_10/common_types.json new file mode 100644 index 0000000000..b01a540627 --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_10/common_types.json @@ -0,0 +1,346 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_10/common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "CallId": { + "type": "string", + "description": "The unique identifier for a server initiated function call." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": [ + "id" + ] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": [ + "componentId", + "path" + ], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "description": "The expected return type of the function call.", + "enum": [ + "array", + "boolean", + "number", + "object", + "string", + "void" + ], + "default": "boolean" + } + }, + "required": [ + "call" + ], + "oneOf": [ + { + "$ref": "catalog.json#/$defs/anyFunction" + } + ] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": [ + "condition", + "message" + ], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + }, + "wantResponse": { + "type": "boolean", + "description": "If true, the client expects an actionResponse from the server.", + "default": false + }, + "responsePath": { + "type": "string", + "description": "Optional JSON Pointer path where the client should save the response value in its local data model." + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "event" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": [ + "functionCall" + ], + "additionalProperties": false + } + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_10/server_to_client.json b/agent_sdks/go/a2uischema/schemas/v0_10/server_to_client.json new file mode 100644 index 0000000000..f491934d41 --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_10/server_to_client.json @@ -0,0 +1,257 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_10/server_to_client.json", + "title": "A2UI Message Schema", + "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", + "oneOf": [ + { + "$ref": "#/$defs/CreateSurfaceMessage" + }, + { + "$ref": "#/$defs/UpdateComponentsMessage" + }, + { + "$ref": "#/$defs/UpdateDataModelMessage" + }, + { + "$ref": "#/$defs/DeleteSurfaceMessage" + }, + { + "$ref": "#/$defs/CallFunctionMessage" + }, + { + "$ref": "#/$defs/ActionResponseMessage" + } + ], + "$defs": { + "CreateSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.10" + }, + "createSurface": { + "type": "object", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be rendered." + }, + "catalogId": { + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" + }, + "theme": { + "$ref": "catalog.json#/$defs/theme", + "description": "Initial theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." + }, + "sendDataModel": { + "type": "boolean", + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." + } + }, + "required": [ + "surfaceId", + "catalogId" + ], + "additionalProperties": false + } + }, + "required": [ + "createSurface", + "version" + ], + "additionalProperties": false + }, + "UpdateComponentsMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.10" + }, + "updateComponents": { + "type": "object", + "description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be updated." + }, + "components": { + "type": "array", + "description": "A list containing all UI components for the surface.", + "minItems": 1, + "items": { + "$ref": "catalog.json#/$defs/anyComponent" + } + } + }, + "required": [ + "surfaceId", + "components" + ], + "additionalProperties": false + } + }, + "required": [ + "updateComponents", + "version" + ], + "additionalProperties": false + }, + "UpdateDataModelMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.10" + }, + "updateDataModel": { + "type": "object", + "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." + }, + "path": { + "type": "string", + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." + }, + "value": { + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true + } + }, + "required": [ + "surfaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "updateDataModel", + "version" + ], + "additionalProperties": false + }, + "DeleteSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.10" + }, + "deleteSurface": { + "type": "object", + "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." + } + }, + "required": [ + "surfaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "deleteSurface", + "version" + ], + "additionalProperties": false + }, + "CallFunctionMessage": { + "type": "object", + "description": "A function invoked from the server.", + "properties": { + "version": { + "const": "v0.10" + }, + "functionCallId": { + "$ref": "common_types.json#/$defs/CallId", + "description": "Unique ID for the instance of this function call. MUST be copied verbatim into the functionResponse or error." + }, + "wantResponse": { + "type": "boolean", + "default": false + }, + "callFunction": { + "$ref": "common_types.json#/$defs/FunctionCall" + } + }, + "required": [ + "version", + "callFunction", + "functionCallId" + ], + "additionalProperties": false + }, + "ActionResponseMessage": { + "type": "object", + "description": "A response to a client-initiated action.", + "properties": { + "version": { + "const": "v0.10" + }, + "actionId": { + "type": "string", + "description": "The ID of the action call this response belongs to." + }, + "actionResponse": { + "type": "object", + "properties": { + "value": { + "description": "The return value of the action.", + "type": [ + "string", + "number", + "boolean", + "array", + "object", + "null" + ] + }, + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "error" + ] + } + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "actionResponse", + "actionId" + ], + "additionalProperties": false + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog.json b/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog.json new file mode 100644 index 0000000000..cefc2b98bb --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog.json @@ -0,0 +1,1383 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "title": "A2UI Basic Catalog", + "description": "Unified catalog of basic A2UI components and functions.", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Text" + }, + "text": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"], + "default": "body" + } + }, + "required": ["component", "text"] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Image" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "description": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "Accessibility text for the image." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": ["contain", "cover", "fill", "none", "scaleDown"], + "default": "fill" + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"], + "default": "mediumFeature" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Icon" + }, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "svgPath": { + "type": "string" + } + }, + "required": ["svgPath"], + "additionalProperties": false + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DataBinding" + } + ] + } + }, + "required": ["component", "name"] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Video" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "AudioPlayer" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": { + "const": "Row" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": { + "const": "Column" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": ["center", "end", "start", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "List" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": ["vertical", "horizontal"], + "default": "vertical" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Card" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": ["component", "child"] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Tabs" + }, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": ["title", "child"], + "additionalProperties": false + } + } + }, + "required": ["component", "tabs"] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Modal" + }, + "trigger": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": ["component", "trigger", "content"] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Divider" + }, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": ["horizontal", "vertical"], + "default": "horizontal" + } + }, + "required": ["component"] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Button" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": ["default", "primary", "borderless"], + "default": "default" + }, + "action": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Action" + } + }, + "required": ["component", "child", "action"] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "TextField" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": ["longText", "number", "shortText", "obscured"], + "default": "shortText" + }, + "validationRegexp": { + "type": "string", + "description": "A regular expression used for client-side validation of the input." + } + }, + "required": ["component", "label"] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "CheckBox" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": ["component", "label", "value"] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": { + "const": "ChoicePicker" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": ["multipleSelection", "mutuallyExclusive"], + "default": "mutuallyExclusive" + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + }, + "displayStyle": { + "type": "string", + "description": "The display style of the component.", + "enum": ["checkbox", "chips"], + "default": "checkbox" + }, + "filterable": { + "type": "boolean", + "description": "If true, displays a search input to filter the options.", + "default": false + } + }, + "required": ["component", "options", "value"] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Slider" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider.", + "default": 0 + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + } + }, + "required": ["component", "value", "max"] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "DateTimeInput" + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date.", + "default": false + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time.", + "default": false + }, + "min": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The maximum allowed date/time in ISO 8601 format." + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": ["component", "value"] + } + ], + "unevaluatedProperties": false + } + }, + "functions": { + "required": { + "type": "object", + "description": "Checks that the value is not null, undefined, or empty.", + "properties": { + "call": { + "const": "required" + }, + "args": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], + "additionalProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "regex": { + "type": "object", + "description": "Checks that the value matches a regular expression string.", + "properties": { + "call": { + "const": "regex" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } + }, + "required": ["value", "pattern"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "length": { + "type": "object", + "description": "Checks string length constraints.", + "properties": { + "call": { + "const": "length" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." + } + }, + "required": ["value"], + "anyOf": [ + { + "required": ["min"] + }, + { + "required": ["max"] + } + ], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "numeric": { + "type": "object", + "description": "Checks numeric range constraints.", + "properties": { + "call": { + "const": "numeric" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber" + }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." + } + }, + "required": ["value"], + "anyOf": [ + { + "required": ["min"] + }, + { + "required": ["max"] + } + ], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "email": { + "type": "object", + "description": "Checks that the value is a valid email address.", + "properties": { + "call": { + "const": "email" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatString": { + "type": "object", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", + "properties": { + "call": { + "const": "formatString" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatNumber": { + "type": "object", + "description": "Formats a number with the specified grouping and decimal precision.", + "properties": { + "call": { + "const": "formatNumber" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatCurrency": { + "type": "object", + "description": "Formats a number as a currency string.", + "properties": { + "call": { + "const": "formatCurrency" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["currency", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatDate": { + "type": "object", + "description": "Formats a timestamp into a string using a pattern.", + "properties": { + "call": { + "const": "formatDate" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + } + }, + "required": ["format", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "pluralize": { + "type": "object", + "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", + "properties": { + "call": { + "const": "pluralize" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." + }, + "zero": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." + } + }, + "required": ["value", "other"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "openUrl": { + "type": "object", + "description": "Opens the specified URL in a browser or handler. This function has no return value.", + "properties": { + "call": { + "const": "openUrl" + }, + "args": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The URL to open." + } + }, + "required": ["url"], + "additionalProperties": false + }, + "returnType": { + "const": "void" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "and": { + "type": "object", + "description": "Performs a logical AND operation on a list of boolean values.", + "properties": { + "call": { + "const": "and" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "or": { + "type": "object", + "description": "Performs a logical OR operation on a list of boolean values.", + "properties": { + "call": { + "const": "or" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "not": { + "type": "object", + "description": "Performs a logical NOT operation on a boolean value.", + "properties": { + "call": { + "const": "not" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "The boolean value to negate." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." + } + } + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + }, + { + "$ref": "#/components/Image" + }, + { + "$ref": "#/components/Icon" + }, + { + "$ref": "#/components/Video" + }, + { + "$ref": "#/components/AudioPlayer" + }, + { + "$ref": "#/components/Row" + }, + { + "$ref": "#/components/Column" + }, + { + "$ref": "#/components/List" + }, + { + "$ref": "#/components/Card" + }, + { + "$ref": "#/components/Tabs" + }, + { + "$ref": "#/components/Modal" + }, + { + "$ref": "#/components/Divider" + }, + { + "$ref": "#/components/Button" + }, + { + "$ref": "#/components/TextField" + }, + { + "$ref": "#/components/CheckBox" + }, + { + "$ref": "#/components/ChoicePicker" + }, + { + "$ref": "#/components/Slider" + }, + { + "$ref": "#/components/DateTimeInput" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "oneOf": [ + { + "$ref": "#/functions/required" + }, + { + "$ref": "#/functions/regex" + }, + { + "$ref": "#/functions/length" + }, + { + "$ref": "#/functions/numeric" + }, + { + "$ref": "#/functions/email" + }, + { + "$ref": "#/functions/formatString" + }, + { + "$ref": "#/functions/formatNumber" + }, + { + "$ref": "#/functions/formatCurrency" + }, + { + "$ref": "#/functions/formatDate" + }, + { + "$ref": "#/functions/pluralize" + }, + { + "$ref": "#/functions/openUrl" + }, + { + "$ref": "#/functions/and" + }, + { + "$ref": "#/functions/or" + }, + { + "$ref": "#/functions/not" + } + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog_rules.txt b/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog_rules.txt new file mode 100644 index 0000000000..33e601348e --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9/basic_catalog_rules.txt @@ -0,0 +1,5 @@ +**REQUIRED PROPERTIES:** You MUST include ALL required properties for every component, even if they are inside a template or will be bound to data. +- For 'Text', you MUST provide 'text'. If dynamic, use { "path": "..." }. +- For 'Image', you MUST provide 'url'. If dynamic, use { "path": "..." }. +- For 'Button', you MUST provide 'action'. +- For 'TextField', 'CheckBox', etc., you MUST provide 'label'. diff --git a/agent_sdks/go/a2uischema/schemas/v0_9/common_types.json b/agent_sdks/go/a2uischema/schemas/v0_9/common_types.json new file mode 100644 index 0000000000..51c5b036bc --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9/common_types.json @@ -0,0 +1,305 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9/server_to_client.json b/agent_sdks/go/a2uischema/schemas/v0_9/server_to_client.json new file mode 100644 index 0000000000..005980642d --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9/server_to_client.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", + "title": "A2UI Message Schema", + "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", + "oneOf": [ + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "createSurface": { + "type": "object", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be rendered." + }, + "catalogId": { + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" + }, + "theme": { + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." + }, + "sendDataModel": { + "type": "boolean", + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." + } + }, + "required": ["surfaceId", "catalogId"], + "additionalProperties": false + } + }, + "required": ["createSurface", "version"], + "additionalProperties": false + }, + "UpdateComponentsMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "updateComponents": { + "type": "object", + "description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be updated." + }, + + "components": { + "type": "array", + "description": "A list containing all UI components for the surface.", + "minItems": 1, + "items": { + "$ref": "catalog.json#/$defs/anyComponent" + } + } + }, + "required": ["surfaceId", "components"], + "additionalProperties": false + } + }, + "required": ["updateComponents", "version"], + "additionalProperties": false + }, + "UpdateDataModelMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "updateDataModel": { + "type": "object", + "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." + }, + "path": { + "type": "string", + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." + }, + "value": { + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true + } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["updateDataModel", "version"], + "additionalProperties": false + }, + "DeleteSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "deleteSurface": { + "type": "object", + "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." + } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["deleteSurface", "version"], + "additionalProperties": false + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog.json b/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog.json new file mode 100644 index 0000000000..cefc2b98bb --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog.json @@ -0,0 +1,1383 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "title": "A2UI Basic Catalog", + "description": "Unified catalog of basic A2UI components and functions.", + "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Text" + }, + "text": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"], + "default": "body" + } + }, + "required": ["component", "text"] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Image" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "description": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "Accessibility text for the image." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": ["contain", "cover", "fill", "none", "scaleDown"], + "default": "fill" + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"], + "default": "mediumFeature" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Icon" + }, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "svgPath": { + "type": "string" + } + }, + "required": ["svgPath"], + "additionalProperties": false + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DataBinding" + } + ] + } + }, + "required": ["component", "name"] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Video" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "AudioPlayer" + }, + "url": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": { + "const": "Row" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": { + "const": "Column" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": ["center", "end", "start", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "List" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": ["vertical", "horizontal"], + "default": "vertical" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": ["start", "center", "end", "stretch"], + "default": "stretch" + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Card" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": ["component", "child"] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Tabs" + }, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": ["title", "child"], + "additionalProperties": false + } + } + }, + "required": ["component", "tabs"] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Modal" + }, + "trigger": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": ["component", "trigger", "content"] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Divider" + }, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": ["horizontal", "vertical"], + "default": "horizontal" + } + }, + "required": ["component"] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Button" + }, + "child": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": ["default", "primary", "borderless"], + "default": "default" + }, + "action": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Action" + } + }, + "required": ["component", "child", "action"] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "TextField" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": ["longText", "number", "shortText", "obscured"], + "default": "shortText" + }, + "validationRegexp": { + "type": "string", + "description": "A regular expression used for client-side validation of the input." + } + }, + "required": ["component", "label"] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "CheckBox" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": ["component", "label", "value"] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": { + "const": "ChoicePicker" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": ["multipleSelection", "mutuallyExclusive"], + "default": "mutuallyExclusive" + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + }, + "displayStyle": { + "type": "string", + "description": "The display style of the component.", + "enum": ["checkbox", "chips"], + "default": "checkbox" + }, + "filterable": { + "type": "boolean", + "description": "If true, displays a search input to filter the options.", + "default": false + } + }, + "required": ["component", "options", "value"] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Slider" + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider.", + "default": 0 + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + } + }, + "required": ["component", "value", "max"] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "DateTimeInput" + }, + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date.", + "default": false + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time.", + "default": false + }, + "min": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The maximum allowed date/time in ISO 8601 format." + }, + "label": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": ["component", "value"] + } + ], + "unevaluatedProperties": false + } + }, + "functions": { + "required": { + "type": "object", + "description": "Checks that the value is not null, undefined, or empty.", + "properties": { + "call": { + "const": "required" + }, + "args": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], + "additionalProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "regex": { + "type": "object", + "description": "Checks that the value matches a regular expression string.", + "properties": { + "call": { + "const": "regex" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } + }, + "required": ["value", "pattern"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "length": { + "type": "object", + "description": "Checks string length constraints.", + "properties": { + "call": { + "const": "length" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." + } + }, + "required": ["value"], + "anyOf": [ + { + "required": ["min"] + }, + { + "required": ["max"] + } + ], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "numeric": { + "type": "object", + "description": "Checks numeric range constraints.", + "properties": { + "call": { + "const": "numeric" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber" + }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." + } + }, + "required": ["value"], + "anyOf": [ + { + "required": ["min"] + }, + { + "required": ["max"] + } + ], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "email": { + "type": "object", + "description": "Checks that the value is a valid email address.", + "properties": { + "call": { + "const": "email" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatString": { + "type": "object", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", + "properties": { + "call": { + "const": "formatString" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString" + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatNumber": { + "type": "object", + "description": "Formats a number with the specified grouping and decimal precision.", + "properties": { + "call": { + "const": "formatNumber" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatCurrency": { + "type": "object", + "description": "Formats a number as a currency string.", + "properties": { + "call": { + "const": "formatCurrency" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["currency", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "formatDate": { + "type": "object", + "description": "Formats a timestamp into a string using a pattern.", + "properties": { + "call": { + "const": "formatDate" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + } + }, + "required": ["format", "value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "pluralize": { + "type": "object", + "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", + "properties": { + "call": { + "const": "pluralize" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." + }, + "zero": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." + } + }, + "required": ["value", "other"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "string" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "openUrl": { + "type": "object", + "description": "Opens the specified URL in a browser or handler. This function has no return value.", + "properties": { + "call": { + "const": "openUrl" + }, + "args": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The URL to open." + } + }, + "required": ["url"], + "additionalProperties": false + }, + "returnType": { + "const": "void" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "and": { + "type": "object", + "description": "Performs a logical AND operation on a list of boolean values.", + "properties": { + "call": { + "const": "and" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "or": { + "type": "object", + "description": "Performs a logical OR operation on a list of boolean values.", + "properties": { + "call": { + "const": "or" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "description": "The list of boolean values to evaluate.", + "items": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean" + }, + "minItems": 2 + } + }, + "required": ["values"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + }, + "not": { + "type": "object", + "description": "Performs a logical NOT operation on a boolean value.", + "properties": { + "call": { + "const": "not" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "https://a2ui.org/specification/v0_9/common_types.json#/$defs/DynamicBoolean", + "description": "The boolean value to negate." + } + }, + "required": ["value"], + "unevaluatedProperties": false + }, + "returnType": { + "const": "boolean" + } + }, + "required": ["call", "args"], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." + } + } + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + }, + { + "$ref": "#/components/Image" + }, + { + "$ref": "#/components/Icon" + }, + { + "$ref": "#/components/Video" + }, + { + "$ref": "#/components/AudioPlayer" + }, + { + "$ref": "#/components/Row" + }, + { + "$ref": "#/components/Column" + }, + { + "$ref": "#/components/List" + }, + { + "$ref": "#/components/Card" + }, + { + "$ref": "#/components/Tabs" + }, + { + "$ref": "#/components/Modal" + }, + { + "$ref": "#/components/Divider" + }, + { + "$ref": "#/components/Button" + }, + { + "$ref": "#/components/TextField" + }, + { + "$ref": "#/components/CheckBox" + }, + { + "$ref": "#/components/ChoicePicker" + }, + { + "$ref": "#/components/Slider" + }, + { + "$ref": "#/components/DateTimeInput" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "oneOf": [ + { + "$ref": "#/functions/required" + }, + { + "$ref": "#/functions/regex" + }, + { + "$ref": "#/functions/length" + }, + { + "$ref": "#/functions/numeric" + }, + { + "$ref": "#/functions/email" + }, + { + "$ref": "#/functions/formatString" + }, + { + "$ref": "#/functions/formatNumber" + }, + { + "$ref": "#/functions/formatCurrency" + }, + { + "$ref": "#/functions/formatDate" + }, + { + "$ref": "#/functions/pluralize" + }, + { + "$ref": "#/functions/openUrl" + }, + { + "$ref": "#/functions/and" + }, + { + "$ref": "#/functions/or" + }, + { + "$ref": "#/functions/not" + } + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog_rules.txt b/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog_rules.txt new file mode 100644 index 0000000000..33e601348e --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9_1/basic_catalog_rules.txt @@ -0,0 +1,5 @@ +**REQUIRED PROPERTIES:** You MUST include ALL required properties for every component, even if they are inside a template or will be bound to data. +- For 'Text', you MUST provide 'text'. If dynamic, use { "path": "..." }. +- For 'Image', you MUST provide 'url'. If dynamic, use { "path": "..." }. +- For 'Button', you MUST provide 'action'. +- For 'TextField', 'CheckBox', etc., you MUST provide 'label'. diff --git a/agent_sdks/go/a2uischema/schemas/v0_9_1/common_types.json b/agent_sdks/go/a2uischema/schemas/v0_9_1/common_types.json new file mode 100644 index 0000000000..51c5b036bc --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9_1/common_types.json @@ -0,0 +1,305 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} diff --git a/agent_sdks/go/a2uischema/schemas/v0_9_1/server_to_client.json b/agent_sdks/go/a2uischema/schemas/v0_9_1/server_to_client.json new file mode 100644 index 0000000000..005980642d --- /dev/null +++ b/agent_sdks/go/a2uischema/schemas/v0_9_1/server_to_client.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", + "title": "A2UI Message Schema", + "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", + "oneOf": [ + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "createSurface": { + "type": "object", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be rendered." + }, + "catalogId": { + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" + }, + "theme": { + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." + }, + "sendDataModel": { + "type": "boolean", + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." + } + }, + "required": ["surfaceId", "catalogId"], + "additionalProperties": false + } + }, + "required": ["createSurface", "version"], + "additionalProperties": false + }, + "UpdateComponentsMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "updateComponents": { + "type": "object", + "description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be updated." + }, + + "components": { + "type": "array", + "description": "A list containing all UI components for the surface.", + "minItems": 1, + "items": { + "$ref": "catalog.json#/$defs/anyComponent" + } + } + }, + "required": ["surfaceId", "components"], + "additionalProperties": false + } + }, + "required": ["updateComponents", "version"], + "additionalProperties": false + }, + "UpdateDataModelMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "updateDataModel": { + "type": "object", + "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." + }, + "path": { + "type": "string", + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." + }, + "value": { + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true + } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["updateDataModel", "version"], + "additionalProperties": false + }, + "DeleteSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "deleteSurface": { + "type": "object", + "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." + } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["deleteSurface", "version"], + "additionalProperties": false + } + } +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/01_flight-status.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/01_flight-status.json new file mode 100644 index 0000000000..0242cc3052 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/01_flight-status.json @@ -0,0 +1,201 @@ +{ + "name": "Flight Status", + "description": "Example of flight status demonstrating date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-flight-status", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-flight-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "route-row", "divider", "times-row"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-left", "date"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "header-left", + "component": "Row", + "children": ["flight-indicator", "flight-number"], + "align": "center" + }, + { + "id": "flight-indicator", + "component": "Icon", + "name": "send" + }, + { + "id": "flight-number", + "component": "Text", + "text": { + "path": "/flightNumber" + }, + "variant": "h3" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "E, MMM d" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "route-row", + "component": "Row", + "children": ["origin", "arrow", "destination"], + "align": "center" + }, + { + "id": "origin", + "component": "Text", + "text": { + "path": "/origin" + }, + "variant": "h2" + }, + { + "id": "arrow", + "component": "Text", + "text": "\u2192", + "variant": "h2" + }, + { + "id": "destination", + "component": "Text", + "text": { + "path": "/destination" + }, + "variant": "h2" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "times-row", + "component": "Row", + "children": ["departure-col", "status-col", "arrival-col"], + "justify": "spaceBetween" + }, + { + "id": "departure-col", + "component": "Column", + "children": ["departure-label", "departure-time"], + "align": "start" + }, + { + "id": "departure-label", + "component": "Text", + "text": "Departs", + "variant": "caption" + }, + { + "id": "departure-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/departureTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "status-col", + "component": "Column", + "children": ["status-label", "status-value"], + "align": "center" + }, + { + "id": "status-label", + "component": "Text", + "text": "Status", + "variant": "caption" + }, + { + "id": "status-value", + "component": "Text", + "text": { + "path": "/status" + }, + "variant": "body" + }, + { + "id": "arrival-col", + "component": "Column", + "children": ["arrival-label", "arrival-time"], + "align": "end" + }, + { + "id": "arrival-label", + "component": "Text", + "text": "Arrives", + "variant": "caption" + }, + { + "id": "arrival-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/arrivalTime" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "h3" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-flight-status", + "value": { + "flightNumber": "OS 87", + "date": "2025-12-15", + "origin": "Vienna", + "destination": "New York", + "departureTime": "2025-12-15T10:15:00Z", + "status": "On Time", + "arrivalTime": "2025-12-15T14:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/02_email-compose.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/02_email-compose.json new file mode 100644 index 0000000000..60338c85e6 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/02_email-compose.json @@ -0,0 +1,185 @@ +{ + "name": "Email Compose", + "description": "Example of email compose", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-email-compose", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-email-compose", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["from-row", "to-row", "subject-row", "divider", "message", "actions"] + }, + { + "id": "from-row", + "component": "Row", + "children": ["from-label", "from-value"], + "align": "center" + }, + { + "id": "from-label", + "component": "Text", + "text": "FROM", + "variant": "caption" + }, + { + "id": "from-value", + "component": "Text", + "text": { + "path": "/from" + }, + "variant": "body" + }, + { + "id": "to-row", + "component": "Row", + "children": ["to-label", "to-value"], + "align": "center" + }, + { + "id": "to-label", + "component": "Text", + "text": "TO", + "variant": "caption" + }, + { + "id": "to-value", + "component": "Text", + "text": { + "path": "/to" + }, + "variant": "body" + }, + { + "id": "subject-row", + "component": "Row", + "children": ["subject-label", "subject-value"], + "align": "center" + }, + { + "id": "subject-label", + "component": "Text", + "text": "SUBJECT", + "variant": "caption" + }, + { + "id": "subject-value", + "component": "Text", + "text": { + "path": "/subject" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "message", + "component": "Column", + "children": ["greeting", "body-text", "closing", "signature"] + }, + { + "id": "greeting", + "component": "Text", + "text": { + "path": "/greeting" + }, + "variant": "body" + }, + { + "id": "body-text", + "component": "Text", + "text": { + "path": "/body" + }, + "variant": "body" + }, + { + "id": "closing", + "component": "Text", + "text": { + "path": "/closing" + }, + "variant": "body" + }, + { + "id": "signature", + "component": "Text", + "text": { + "path": "/signature" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["send-btn", "discard-btn"] + }, + { + "id": "send-btn-text", + "component": "Text", + "text": "Send email" + }, + { + "id": "send-btn", + "component": "Button", + "child": "send-btn-text", + "action": { + "event": { + "name": "send", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-email-compose", + "value": { + "from": "alex@acme.com", + "to": "jordan@acme.com", + "subject": "Q4 Revenue Forecast", + "greeting": "Hi Jordan,", + "body": "Following up on our call. Please review the attached Q4 forecast and let me know if you have questions before the board meeting.", + "closing": "Best,", + "signature": "Alex" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/03_calendar-day.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/03_calendar-day.json new file mode 100644 index 0000000000..a7bd098819 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/03_calendar-day.json @@ -0,0 +1,166 @@ +{ + "name": "Calendar Day", + "description": "Example of calendar day demonstrating dynamic templating, relative paths, and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-calendar-day", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-calendar-day", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "divider", "actions"] + }, + { + "id": "header-row", + "component": "Row", + "children": ["date-col", "events-col"] + }, + { + "id": "date-col", + "component": "Column", + "children": ["day-name", "day-number"], + "align": "start" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-number", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "d" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "events-col", + "component": "Column", + "children": { + "path": "/events", + "componentId": "event-template" + } + }, + { + "id": "event-template", + "component": "Column", + "children": ["event-title", "event-time"] + }, + { + "id": "event-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "event-time", + "component": "Text", + "text": { + "path": "time" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-btn", "discard-btn"] + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to calendar" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add", + "context": {} + } + } + }, + { + "id": "discard-btn-text", + "component": "Text", + "text": "Discard" + }, + { + "id": "discard-btn", + "component": "Button", + "child": "discard-btn-text", + "action": { + "event": { + "name": "discard", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-calendar-day", + "value": { + "date": "2025-12-28", + "events": [ + { + "title": "Lunch", + "time": "12:00 - 12:45 PM" + }, + { + "title": "Q1 roadmap review", + "time": "1:00 - 2:00 PM" + }, + { + "title": "Team standup", + "time": "3:30 - 4:00 PM" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/04_weather-current.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/04_weather-current.json new file mode 100644 index 0000000000..4d14ed30a3 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/04_weather-current.json @@ -0,0 +1,168 @@ +{ + "name": "Weather Current", + "description": "Example of weather current demonstrating templating and string formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-weather-current", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-weather-current", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["temp-row", "location", "description", "forecast-row"], + "align": "center" + }, + { + "id": "temp-row", + "component": "Row", + "children": ["temp-high", "temp-low"], + "align": "start" + }, + { + "id": "temp-high", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempHigh}°" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "temp-low", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/tempLow}°" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "location", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "caption" + }, + { + "id": "forecast-row", + "component": "Row", + "children": { + "path": "/forecast", + "componentId": "forecast-day-template" + }, + "justify": "spaceAround" + }, + { + "id": "forecast-day-template", + "component": "Column", + "children": ["day-name", "day-icon", "day-temp"], + "align": "center" + }, + { + "id": "day-name", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "date" + }, + "format": "E" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "day-icon", + "component": "Text", + "text": { + "path": "icon" + }, + "variant": "h3" + }, + { + "id": "day-temp", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${temp}°" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-weather-current", + "value": { + "tempHigh": 72, + "tempLow": 58, + "location": "Austin, TX", + "description": "Clear skies with light breeze", + "forecast": [ + { + "date": "2025-12-16", + "icon": "☀️", + "temp": 74 + }, + { + "date": "2025-12-17", + "icon": "☀️", + "temp": 76 + }, + { + "date": "2025-12-18", + "icon": "⛅", + "temp": 71 + }, + { + "date": "2025-12-19", + "icon": "☀️", + "temp": 73 + }, + { + "date": "2025-12-20", + "icon": "☀️", + "temp": 75 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/05_product-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/05_product-card.json new file mode 100644 index 0000000000..98c4f9762f --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/05_product-card.json @@ -0,0 +1,151 @@ +{ + "name": "Product Card", + "description": "Example of product card demonstrating currency formatting and pluralization.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-product-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-product-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["image", "details"] + }, + { + "id": "image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "details", + "component": "Column", + "children": ["name", "rating-row", "price-row", "actions"] + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["stars", "reviews"], + "align": "center" + }, + { + "id": "stars", + "component": "Text", + "text": { + "path": "/stars" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "price-row", + "component": "Row", + "children": ["price", "original-price"], + "align": "start" + }, + { + "id": "price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "original-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/originalPrice" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "actions", + "component": "Row", + "children": ["add-cart-btn"] + }, + { + "id": "add-cart-btn-text", + "component": "Text", + "text": "Add to Cart" + }, + { + "id": "add-cart-btn", + "component": "Button", + "child": "add-cart-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "addToCart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-product-card", + "value": { + "imageUrl": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&h=200&fit=crop", + "name": "Wireless Headphones Pro", + "stars": "★★★★★", + "reviewCount": 2847, + "price": 199.99, + "originalPrice": 249.99 + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/06_music-player.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/06_music-player.json new file mode 100644 index 0000000000..c83d3843e7 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/06_music-player.json @@ -0,0 +1,165 @@ +{ + "name": "Music Player", + "description": "Example of music player", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-music-player", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-music-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["album-art", "track-info", "progress", "time-row", "controls"], + "align": "center" + }, + { + "id": "album-art", + "component": "Image", + "url": { + "path": "/albumArt" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["song-title", "artist"], + "align": "center" + }, + { + "id": "song-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "artist", + "component": "Text", + "text": { + "path": "/artist" + }, + "variant": "caption" + }, + { + "id": "progress", + "component": "Slider", + "value": { + "path": "/progress" + }, + "max": 1 + }, + { + "id": "time-row", + "component": "Row", + "children": ["current-time", "total-time"], + "justify": "spaceBetween" + }, + { + "id": "current-time", + "component": "Text", + "text": { + "path": "/currentTime" + }, + "variant": "caption" + }, + { + "id": "total-time", + "component": "Text", + "text": { + "path": "/totalTime" + }, + "variant": "caption" + }, + { + "id": "controls", + "component": "Row", + "children": ["prev-btn", "play-btn", "next-btn"], + "justify": "center" + }, + { + "id": "prev-btn-icon", + "component": "Icon", + "name": "skipPrevious" + }, + { + "id": "prev-btn", + "component": "Button", + "child": "prev-btn-icon", + "action": { + "event": { + "name": "previous", + "context": {} + } + } + }, + { + "id": "play-btn-icon", + "component": "Icon", + "name": { + "path": "/playIcon" + } + }, + { + "id": "play-btn", + "component": "Button", + "child": "play-btn-icon", + "action": { + "event": { + "name": "playPause", + "context": {} + } + } + }, + { + "id": "next-btn-icon", + "component": "Icon", + "name": "skipNext" + }, + { + "id": "next-btn", + "component": "Button", + "child": "next-btn-icon", + "action": { + "event": { + "name": "next", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-music-player", + "value": { + "albumArt": "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=300&h=300&fit=crop", + "title": "Blinding Lights", + "artist": "The Weeknd", + "album": "After Hours", + "progress": 0.45, + "currentTime": "1:48", + "totalTime": "4:22", + "playIcon": "pause" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/07_task-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/07_task-card.json new file mode 100644 index 0000000000..37296e01ba --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/07_task-card.json @@ -0,0 +1,107 @@ +{ + "name": "Task Card", + "description": "Example of task card demonstrating CheckBox and DateTimeInput.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-task-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-task-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["status-checkbox", "content", "priority"], + "align": "start" + }, + { + "id": "status-checkbox", + "component": "CheckBox", + "label": "", + "value": { + "path": "/completed" + } + }, + { + "id": "content", + "component": "Column", + "children": ["title", "description", "meta-row"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["due-date-input", "project"], + "align": "center" + }, + { + "id": "due-date-input", + "component": "DateTimeInput", + "label": "Due", + "value": { + "path": "/dueDate" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "project", + "component": "Text", + "text": { + "path": "/project" + }, + "variant": "caption" + }, + { + "id": "priority", + "component": "Icon", + "name": { + "path": "/priorityIcon" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-task-card", + "value": { + "completed": false, + "title": "Review pull request", + "description": "Review and approve the authentication module changes.", + "dueDate": "2025-12-15T17:00:00Z", + "project": "Backend", + "priorityIcon": "priority_high" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/08_user-profile.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/08_user-profile.json new file mode 100644 index 0000000000..8bea67a025 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/08_user-profile.json @@ -0,0 +1,190 @@ +{ + "name": "User Profile", + "description": "Example of user profile demonstrating number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-user-profile", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-user-profile", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "info", "bio", "stats-row", "follow-btn"], + "align": "center" + }, + { + "id": "header", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "info", + "component": "Column", + "children": ["name", "username"], + "align": "center" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "username", + "component": "Text", + "text": { + "path": "/username" + }, + "variant": "caption" + }, + { + "id": "bio", + "component": "Text", + "text": { + "path": "/bio" + }, + "variant": "body" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["followers-col", "following-col", "posts-col"], + "justify": "spaceAround" + }, + { + "id": "followers-col", + "component": "Column", + "children": ["followers-count", "followers-label"], + "align": "center" + }, + { + "id": "followers-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/followers" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "followers-label", + "component": "Text", + "text": "Followers", + "variant": "caption" + }, + { + "id": "following-col", + "component": "Column", + "children": ["following-count", "following-label"], + "align": "center" + }, + { + "id": "following-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/following" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "following-label", + "component": "Text", + "text": "Following", + "variant": "caption" + }, + { + "id": "posts-col", + "component": "Column", + "children": ["posts-count", "posts-label"], + "align": "center" + }, + { + "id": "posts-count", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/posts" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "posts-label", + "component": "Text", + "text": "Posts", + "variant": "caption" + }, + { + "id": "follow-btn-text", + "component": "Text", + "text": { + "path": "/followText" + } + }, + { + "id": "follow-btn", + "component": "Button", + "child": "follow-btn-text", + "action": { + "event": { + "name": "follow", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-user-profile", + "value": { + "avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop", + "name": "Sarah Chen", + "username": "@sarahchen", + "bio": "Product Designer at Tech Co. Creating delightful experiences.", + "followers": 12400, + "following": 892, + "posts": 347, + "followText": "Follow" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/09_login-form.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/09_login-form.json new file mode 100644 index 0000000000..2edaa23453 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/09_login-form.json @@ -0,0 +1,214 @@ +{ + "name": "Login Form with Validation", + "description": "Example of login form demonstrating validation checks and logic.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-login-form", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-login-form", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header", + "email-field", + "password-field", + "login-btn", + "divider", + "signup-text" + ] + }, + { + "id": "header", + "component": "Column", + "children": ["title", "subtitle"], + "align": "center" + }, + { + "id": "title", + "component": "Text", + "text": "Welcome back", + "variant": "h2" + }, + { + "id": "subtitle", + "component": "Text", + "text": "Sign in to your account", + "variant": "caption" + }, + { + "id": "email-field", + "component": "TextField", + "value": { + "path": "/email" + }, + "label": "Email", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Email is required" + }, + { + "condition": { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + "message": "Please enter a valid email address" + } + ] + }, + { + "id": "password-field", + "component": "TextField", + "value": { + "path": "/password" + }, + "label": "Password", + "variant": "obscured", + "checks": [ + { + "condition": { + "call": "required", + "args": { + "value": { + "path": "/password" + } + } + }, + "message": "Password is required" + }, + { + "condition": { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + }, + "message": "Password must be at least 8 characters long" + } + ] + }, + { + "id": "login-btn-text", + "component": "Text", + "text": "Sign in" + }, + { + "id": "login-btn", + "component": "Button", + "child": "login-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + { + "call": "email", + "args": { + "value": { + "path": "/email" + } + } + }, + { + "call": "length", + "args": { + "value": { + "path": "/password" + }, + "min": 8 + } + } + ] + } + }, + "message": "Please fix errors before signing in" + } + ], + "action": { + "event": { + "name": "login", + "context": { + "email": { + "path": "/email" + } + } + } + } + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "signup-text", + "component": "Row", + "children": ["no-account", "signup-link"], + "justify": "center" + }, + { + "id": "no-account", + "component": "Text", + "text": "Don't have an account?", + "variant": "caption" + }, + { + "id": "signup-link-text", + "component": "Text", + "text": "Sign up" + }, + { + "id": "signup-link", + "component": "Button", + "child": "signup-link-text", + "action": { + "event": { + "name": "signup", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-login-form", + "value": { + "email": "", + "password": "" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/10_notification-permission.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/10_notification-permission.json new file mode 100644 index 0000000000..0eb9b1d468 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/10_notification-permission.json @@ -0,0 +1,105 @@ +{ + "name": "Notification Permission", + "description": "Example of notification permission", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-notification-permission", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-notification-permission", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["icon", "title", "description", "actions"], + "align": "center" + }, + { + "id": "icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["yes-btn", "no-btn"], + "justify": "center" + }, + { + "id": "yes-btn-text", + "component": "Text", + "text": "Yes" + }, + { + "id": "yes-btn", + "component": "Button", + "child": "yes-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "no-btn-text", + "component": "Text", + "text": "No" + }, + { + "id": "no-btn", + "component": "Button", + "child": "no-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-notification-permission", + "value": { + "icon": "check", + "title": "Enable notification", + "description": "Get alerts for order status changes" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/11_purchase-complete.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/11_purchase-complete.json new file mode 100644 index 0000000000..c9ecce6e3e --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/11_purchase-complete.json @@ -0,0 +1,169 @@ +{ + "name": "Purchase Complete", + "description": "Example of purchase complete demonstrating currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-purchase-complete", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-purchase-complete", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "success-icon", + "title", + "product-row", + "divider", + "details-col", + "view-btn" + ], + "align": "center" + }, + { + "id": "success-icon", + "component": "Icon", + "name": "check" + }, + { + "id": "title", + "component": "Text", + "text": "Purchase Complete", + "variant": "h2" + }, + { + "id": "product-row", + "component": "Row", + "children": ["product-image", "product-info"], + "align": "center" + }, + { + "id": "product-image", + "component": "Image", + "url": { + "path": "/productImage" + }, + "fit": "cover" + }, + { + "id": "product-info", + "component": "Column", + "children": ["product-name", "product-price"] + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h4" + }, + { + "id": "product-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "details-col", + "component": "Column", + "children": ["delivery-row", "seller-row"] + }, + { + "id": "delivery-row", + "component": "Row", + "children": ["delivery-icon", "delivery-text"], + "align": "center" + }, + { + "id": "delivery-icon", + "component": "Icon", + "name": "arrowForward" + }, + { + "id": "delivery-text", + "component": "Text", + "text": { + "path": "/deliveryDate" + }, + "variant": "body" + }, + { + "id": "seller-row", + "component": "Row", + "children": ["seller-label", "seller-name"] + }, + { + "id": "seller-label", + "component": "Text", + "text": "Sold by:", + "variant": "caption" + }, + { + "id": "seller-name", + "component": "Text", + "text": { + "path": "/seller" + }, + "variant": "body" + }, + { + "id": "view-btn-text", + "component": "Text", + "text": "View Order Details" + }, + { + "id": "view-btn", + "component": "Button", + "child": "view-btn-text", + "action": { + "event": { + "name": "view_details", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-purchase-complete", + "value": { + "productImage": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&h=100&fit=crop", + "productName": "Wireless Headphones Pro", + "price": 199.99, + "deliveryDate": "Arrives Dec 18 - Dec 20", + "seller": "TechStore Official" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/12_chat-message.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/12_chat-message.json new file mode 100644 index 0000000000..f517eb1c87 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/12_chat-message.json @@ -0,0 +1,144 @@ +{ + "name": "Chat Message", + "description": "Example of chat message demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-chat-message", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-chat-message", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "messages-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["channel-icon", "channel-name"], + "align": "center" + }, + { + "id": "channel-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "channel-name", + "component": "Text", + "text": { + "path": "/channelName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "messages-list", + "component": "Column", + "children": { + "path": "/messages", + "componentId": "message-template" + }, + "align": "start" + }, + { + "id": "message-template", + "component": "Row", + "children": ["msg-avatar", "msg-content"], + "align": "start" + }, + { + "id": "msg-avatar", + "component": "Image", + "url": { + "path": "avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "msg-content", + "component": "Column", + "children": ["msg-header", "msg-text"] + }, + { + "id": "msg-header", + "component": "Row", + "children": ["msg-username", "msg-time"], + "align": "center" + }, + { + "id": "msg-username", + "component": "Text", + "text": { + "path": "username" + }, + "variant": "h4" + }, + { + "id": "msg-time", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "timestamp" + }, + "format": "h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "msg-text", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-chat-message", + "value": { + "channelName": "project-updates", + "messages": [ + { + "avatar": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop", + "username": "Mike Chen", + "timestamp": "2025-12-15T10:32:00Z", + "text": "Just pushed the new API changes. Ready for review." + }, + { + "avatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=40&h=40&fit=crop", + "username": "Sarah Kim", + "timestamp": "2025-12-15T10:45:00Z", + "text": "Great! I'll take a look after standup." + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/13_coffee-order.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/13_coffee-order.json new file mode 100644 index 0000000000..b92dcadb29 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/13_coffee-order.json @@ -0,0 +1,253 @@ +{ + "name": "Coffee Order", + "description": "Example of coffee order demonstrating templating and currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-coffee-order", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-coffee-order", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "items-list", "divider", "totals", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["coffee-icon", "store-name"], + "align": "center" + }, + { + "id": "coffee-icon", + "component": "Icon", + "name": "favorite" + }, + { + "id": "store-name", + "component": "Text", + "text": { + "path": "/storeName" + }, + "variant": "h3" + }, + { + "id": "items-list", + "component": "Column", + "children": { + "path": "/items", + "componentId": "order-item-template" + } + }, + { + "id": "order-item-template", + "component": "Row", + "children": ["item-details", "item-price"], + "justify": "spaceBetween", + "align": "start" + }, + { + "id": "item-details", + "component": "Column", + "children": ["item-name", "item-size"] + }, + { + "id": "item-name", + "component": "Text", + "text": { + "path": "name" + }, + "variant": "body" + }, + { + "id": "item-size", + "component": "Text", + "text": { + "path": "size" + }, + "variant": "caption" + }, + { + "id": "item-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "price" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "totals", + "component": "Column", + "children": ["subtotal-row", "tax-row", "total-row"] + }, + { + "id": "subtotal-row", + "component": "Row", + "children": ["subtotal-label", "subtotal-value"], + "justify": "spaceBetween" + }, + { + "id": "subtotal-label", + "component": "Text", + "text": "Subtotal", + "variant": "caption" + }, + { + "id": "subtotal-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/subtotal" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "tax-row", + "component": "Row", + "children": ["tax-label", "tax-value"], + "justify": "spaceBetween" + }, + { + "id": "tax-label", + "component": "Text", + "text": "Tax", + "variant": "caption" + }, + { + "id": "tax-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/tax" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/total" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h4" + }, + { + "id": "actions", + "component": "Row", + "children": ["purchase-btn", "add-btn"] + }, + { + "id": "purchase-btn-text", + "component": "Text", + "text": "Purchase" + }, + { + "id": "purchase-btn", + "component": "Button", + "child": "purchase-btn-text", + "action": { + "event": { + "name": "purchase", + "context": {} + } + } + }, + { + "id": "add-btn-text", + "component": "Text", + "text": "Add to cart" + }, + { + "id": "add-btn", + "component": "Button", + "child": "add-btn-text", + "action": { + "event": { + "name": "add_to_cart", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-coffee-order", + "value": { + "storeName": "Sunrise Coffee", + "items": [ + { + "name": "Oat Milk Latte", + "size": "Grande, Extra Shot", + "price": 6.45 + }, + { + "name": "Chocolate Croissant", + "size": "Warmed", + "price": 4.25 + } + ], + "subtotal": 10.7, + "tax": 0.96, + "total": 11.66 + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/14_sports-player.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/14_sports-player.json new file mode 100644 index 0000000000..a1fe09b17c --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/14_sports-player.json @@ -0,0 +1,177 @@ +{ + "name": "Sports Player", + "description": "Example of sports player", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-sports-player", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-sports-player", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["player-image", "player-info", "divider", "stats-row"], + "align": "center" + }, + { + "id": "player-image", + "component": "Image", + "url": { + "path": "/playerImage" + }, + "fit": "cover" + }, + { + "id": "player-info", + "component": "Column", + "children": ["player-name", "player-details"], + "align": "center" + }, + { + "id": "player-name", + "component": "Text", + "text": { + "path": "/playerName" + }, + "variant": "h2" + }, + { + "id": "player-details", + "component": "Row", + "children": ["player-number", "player-team"], + "align": "center" + }, + { + "id": "player-number", + "component": "Text", + "text": { + "path": "/number" + }, + "variant": "h3" + }, + { + "id": "player-team", + "component": "Text", + "text": { + "path": "/team" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["stat1", "stat2", "stat3"], + "justify": "spaceAround" + }, + { + "id": "stat1", + "component": "Column", + "children": ["stat1-value", "stat1-label"], + "align": "center" + }, + { + "id": "stat1-value", + "component": "Text", + "text": { + "path": "/stat1/value" + }, + "variant": "h3" + }, + { + "id": "stat1-label", + "component": "Text", + "text": { + "path": "/stat1/label" + }, + "variant": "caption" + }, + { + "id": "stat2", + "component": "Column", + "children": ["stat2-value", "stat2-label"], + "align": "center" + }, + { + "id": "stat2-value", + "component": "Text", + "text": { + "path": "/stat2/value" + }, + "variant": "h3" + }, + { + "id": "stat2-label", + "component": "Text", + "text": { + "path": "/stat2/label" + }, + "variant": "caption" + }, + { + "id": "stat3", + "component": "Column", + "children": ["stat3-value", "stat3-label"], + "align": "center" + }, + { + "id": "stat3-value", + "component": "Text", + "text": { + "path": "/stat3/value" + }, + "variant": "h3" + }, + { + "id": "stat3-label", + "component": "Text", + "text": { + "path": "/stat3/label" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-sports-player", + "value": { + "playerImage": "https://images.unsplash.com/photo-1546519638-68e109498ffc?w=200&h=200&fit=crop", + "playerName": "Marcus Johnson", + "number": "#23", + "team": "LA Lakers", + "stat1": { + "value": "28.4", + "label": "PPG" + }, + "stat2": { + "value": "7.2", + "label": "RPG" + }, + "stat3": { + "value": "6.8", + "label": "APG" + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/15_account-balance.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/15_account-balance.json new file mode 100644 index 0000000000..b22b24b0b5 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/15_account-balance.json @@ -0,0 +1,126 @@ +{ + "name": "Account Balance", + "description": "Example of account balance demonstrating currency formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-account-balance", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-account-balance", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "balance", "updated", "divider", "actions"] + }, + { + "id": "header", + "component": "Row", + "children": ["account-icon", "account-name"], + "align": "center" + }, + { + "id": "account-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "account-name", + "component": "Text", + "text": { + "path": "/accountName" + }, + "variant": "h4" + }, + { + "id": "balance", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/balance" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "updated", + "component": "Text", + "text": { + "path": "/lastUpdated" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["transfer-btn", "pay-btn"] + }, + { + "id": "transfer-btn-text", + "component": "Text", + "text": "Transfer" + }, + { + "id": "transfer-btn", + "component": "Button", + "child": "transfer-btn-text", + "action": { + "event": { + "name": "transfer", + "context": {} + } + } + }, + { + "id": "pay-btn-text", + "component": "Text", + "text": "Pay Bill" + }, + { + "id": "pay-btn", + "component": "Button", + "child": "pay-btn-text", + "action": { + "event": { + "name": "pay_bill", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-account-balance", + "value": { + "accountName": "Primary Checking", + "balance": 12458.32, + "lastUpdated": "Updated just now" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/16_workout-summary.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/16_workout-summary.json new file mode 100644 index 0000000000..6db0735295 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/16_workout-summary.json @@ -0,0 +1,160 @@ +{ + "name": "Workout Summary", + "description": "Example of workout summary demonstrating number and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-workout-summary", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-workout-summary", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "metrics-row", "date"] + }, + { + "id": "header", + "component": "Row", + "children": ["workout-icon", "title"], + "align": "center" + }, + { + "id": "workout-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "title", + "component": "Text", + "text": "Workout Complete", + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "metrics-row", + "component": "Row", + "children": ["duration-col", "calories-col", "distance-col"], + "justify": "spaceAround" + }, + { + "id": "duration-col", + "component": "Column", + "children": ["duration-value", "duration-label"], + "align": "center" + }, + { + "id": "duration-value", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "h3" + }, + { + "id": "duration-label", + "component": "Text", + "text": "Duration", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} km" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "EEEE, MMM d 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-workout-summary", + "value": { + "icon": "directions_run", + "workoutType": "Morning Run", + "duration": "32:15", + "calories": 385, + "distance": 5.2, + "date": "2025-12-15T07:30:00Z" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/17_event-detail.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/17_event-detail.json new file mode 100644 index 0000000000..a62fbbb995 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/17_event-detail.json @@ -0,0 +1,144 @@ +{ + "name": "Event Detail", + "description": "Example of event detail demonstrating date and string formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-event-detail", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-event-detail", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title", "time-row", "location-row", "description", "divider", "actions"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h2" + }, + { + "id": "time-row", + "component": "Row", + "children": ["time-icon", "time-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "time-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatDate(value: ${/start}, format: 'E, MMM d')} • ${formatDate(value: ${/start}, format: 'h:mm a')} - ${formatDate(value: ${/end}, format: 'h:mm a')}" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["accept-btn", "decline-btn"] + }, + { + "id": "accept-btn-text", + "component": "Text", + "text": "Accept" + }, + { + "id": "accept-btn", + "component": "Button", + "child": "accept-btn-text", + "action": { + "event": { + "name": "accept", + "context": {} + } + } + }, + { + "id": "decline-btn-text", + "component": "Text", + "text": "Decline" + }, + { + "id": "decline-btn", + "component": "Button", + "child": "decline-btn-text", + "action": { + "event": { + "name": "decline", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-event-detail", + "value": { + "title": "Product Launch Meeting", + "start": "2025-12-19T14:00:00Z", + "end": "2025-12-19T15:30:00Z", + "location": "Conference Room A, Building 2", + "description": "Review final product specs and marketing materials before the Q1 launch." + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/18_track-list.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/18_track-list.json new file mode 100644 index 0000000000..19a2d6853a --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/18_track-list.json @@ -0,0 +1,152 @@ +{ + "name": "Track List", + "description": "Example of track list demonstrating templating, relative paths, and number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-track-list", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-track-list", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "divider", "tracks-list"] + }, + { + "id": "header", + "component": "Row", + "children": ["playlist-icon", "playlist-name"], + "align": "center" + }, + { + "id": "playlist-icon", + "component": "Icon", + "name": "play" + }, + { + "id": "playlist-name", + "component": "Text", + "text": { + "path": "/playlistName" + }, + "variant": "h3" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "tracks-list", + "component": "Column", + "children": { + "path": "/tracks", + "componentId": "track-item-template" + } + }, + { + "id": "track-item-template", + "component": "Row", + "children": ["track-num", "track-art", "track-info", "track-duration"], + "align": "center" + }, + { + "id": "track-num", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "number" + } + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "track-art", + "component": "Image", + "url": { + "path": "art" + }, + "fit": "cover" + }, + { + "id": "track-info", + "component": "Column", + "children": ["track-title", "track-artist"] + }, + { + "id": "track-title", + "component": "Text", + "text": { + "path": "title" + }, + "variant": "body" + }, + { + "id": "track-artist", + "component": "Text", + "text": { + "path": "artist" + }, + "variant": "caption" + }, + { + "id": "track-duration", + "component": "Text", + "text": { + "path": "duration" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-track-list", + "value": { + "playlistName": "Focus Flow", + "tracks": [ + { + "number": 1, + "art": "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=50&h=50&fit=crop", + "title": "Weightless", + "artist": "Marconi Union", + "duration": "8:09" + }, + { + "number": 2, + "art": "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=50&h=50&fit=crop", + "title": "Clair de Lune", + "artist": "Debussy", + "duration": "5:12" + }, + { + "number": 3, + "art": "https://images.unsplash.com/photo-1507838153414-b4b713384a76?w=50&h=50&fit=crop", + "title": "Ambient Light", + "artist": "Brian Eno", + "duration": "6:45" + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/19_software-purchase.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/19_software-purchase.json new file mode 100644 index 0000000000..2c38be2d63 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/19_software-purchase.json @@ -0,0 +1,194 @@ +{ + "name": "Software Purchase", + "description": "Example of software purchase demonstrating currency formatting and ChoicePicker.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-software-purchase", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-software-purchase", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "title", + "product-name", + "divider1", + "options", + "divider2", + "total-row", + "actions" + ] + }, + { + "id": "title", + "component": "Text", + "text": "Purchase License", + "variant": "h3" + }, + { + "id": "product-name", + "component": "Text", + "text": { + "path": "/productName" + }, + "variant": "h2" + }, + { + "id": "divider1", + "component": "Divider" + }, + { + "id": "options", + "component": "Column", + "children": ["seats-row", "period-row"] + }, + { + "id": "seats-row", + "component": "Row", + "children": ["seats-label", "seats-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "seats-label", + "component": "Text", + "text": "Number of seats", + "variant": "body" + }, + { + "id": "seats-value", + "component": "Text", + "text": { + "path": "/seats" + }, + "variant": "h4" + }, + { + "id": "period-row", + "component": "Row", + "children": ["period-label", "period-picker"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "period-label", + "component": "Text", + "text": "Billing period", + "variant": "body" + }, + { + "id": "period-picker", + "component": "ChoicePicker", + "options": [ + { + "label": "Annual", + "value": "annual" + }, + { + "label": "Monthly", + "value": "monthly" + } + ], + "value": { + "path": "/billingPeriod" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "divider2", + "component": "Divider" + }, + { + "id": "total-row", + "component": "Row", + "children": ["total-label", "total-value"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "total-label", + "component": "Text", + "text": "Total", + "variant": "h4" + }, + { + "id": "total-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${formatCurrency(value: ${/total}, currency: 'USD')}/year" + }, + "returnType": "string" + }, + "variant": "h2" + }, + { + "id": "actions", + "component": "Row", + "children": ["confirm-btn", "cancel-btn"] + }, + { + "id": "confirm-btn-text", + "component": "Text", + "text": "Confirm Purchase" + }, + { + "id": "confirm-btn", + "component": "Button", + "child": "confirm-btn-text", + "action": { + "event": { + "name": "confirm", + "context": {} + } + } + }, + { + "id": "cancel-btn-text", + "component": "Text", + "text": "Cancel" + }, + { + "id": "cancel-btn", + "component": "Button", + "child": "cancel-btn-text", + "action": { + "event": { + "name": "cancel", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-software-purchase", + "value": { + "productName": "Design Suite Pro", + "seats": "10 seats", + "billingPeriod": ["annual"], + "total": 1188.0 + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/20_restaurant-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/20_restaurant-card.json new file mode 100644 index 0000000000..ea4b78c715 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/20_restaurant-card.json @@ -0,0 +1,140 @@ +{ + "name": "Restaurant Card", + "description": "Example of restaurant card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-restaurant-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-restaurant-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["restaurant-image", "content"] + }, + { + "id": "restaurant-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["name-row", "cuisine", "rating-row", "details-row"] + }, + { + "id": "name-row", + "component": "Row", + "children": ["restaurant-name", "price-range"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "restaurant-name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h3" + }, + { + "id": "price-range", + "component": "Text", + "text": { + "path": "/priceRange" + }, + "variant": "body" + }, + { + "id": "cuisine", + "component": "Text", + "text": { + "path": "/cuisine" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "reviews"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "reviews", + "component": "Text", + "text": { + "path": "/reviewCount" + }, + "variant": "caption" + }, + { + "id": "details-row", + "component": "Row", + "children": ["distance", "delivery-time"] + }, + { + "id": "distance", + "component": "Text", + "text": { + "path": "/distance" + }, + "variant": "caption" + }, + { + "id": "delivery-time", + "component": "Text", + "text": { + "path": "/deliveryTime" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-restaurant-card", + "value": { + "image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=300&h=150&fit=crop", + "name": "The Italian Kitchen", + "priceRange": "$$$", + "cuisine": "Italian \u2022 Pasta \u2022 Wine Bar", + "rating": "4.8", + "reviewCount": "(2,847 reviews)", + "distance": "0.8 mi", + "deliveryTime": "25-35 min" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/21_shipping-status.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/21_shipping-status.json new file mode 100644 index 0000000000..afa6b645fd --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/21_shipping-status.json @@ -0,0 +1,137 @@ +{ + "name": "Shipping Status", + "description": "Example of shipping status demonstrating templating and relative paths.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-shipping-status", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-shipping-status", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "tracking-number", "divider", "steps-list", "eta"] + }, + { + "id": "header", + "component": "Row", + "children": ["package-icon", "title"], + "align": "center" + }, + { + "id": "package-icon", + "component": "Icon", + "name": "info" + }, + { + "id": "title", + "component": "Text", + "text": "Package Status", + "variant": "h3" + }, + { + "id": "tracking-number", + "component": "Text", + "text": { + "path": "/trackingNumber" + }, + "variant": "caption" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "steps-list", + "component": "Column", + "children": { + "path": "/steps", + "componentId": "step-item-template" + } + }, + { + "id": "step-item-template", + "component": "Row", + "children": ["step-icon", "step-text"], + "align": "center" + }, + { + "id": "step-icon", + "component": "Icon", + "name": { + "path": "icon" + } + }, + { + "id": "step-text", + "component": "Text", + "text": { + "path": "label" + }, + "variant": "body" + }, + { + "id": "eta", + "component": "Row", + "children": ["eta-icon", "eta-text"], + "align": "center" + }, + { + "id": "eta-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "eta-text", + "component": "Text", + "text": { + "path": "/eta" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-shipping-status", + "value": { + "trackingNumber": "Tracking: 1Z999AA10123456784", + "steps": [ + { + "icon": "check", + "label": "Order Placed" + }, + { + "icon": "check", + "label": "Shipped" + }, + { + "icon": "send", + "label": "Out for Delivery" + }, + { + "icon": "check", + "label": "Delivered" + } + ], + "eta": "Estimated delivery: Today by 8 PM" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/22_credit-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/22_credit-card.json new file mode 100644 index 0000000000..639506e953 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/22_credit-card.json @@ -0,0 +1,117 @@ +{ + "name": "Credit Card", + "description": "Example of credit card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-credit-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-credit-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["card-type-row", "card-number", "card-details"] + }, + { + "id": "card-type-row", + "component": "Row", + "children": ["card-icon", "card-type"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "card-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "card-type", + "component": "Text", + "text": { + "path": "/cardType" + }, + "variant": "h4" + }, + { + "id": "card-number", + "component": "Text", + "text": { + "path": "/cardNumber" + }, + "variant": "h2" + }, + { + "id": "card-details", + "component": "Row", + "children": ["holder-col", "expiry-col"], + "justify": "spaceBetween" + }, + { + "id": "holder-col", + "component": "Column", + "children": ["holder-label", "holder-name"] + }, + { + "id": "holder-label", + "component": "Text", + "text": "CARD HOLDER", + "variant": "caption" + }, + { + "id": "holder-name", + "component": "Text", + "text": { + "path": "/holderName" + }, + "variant": "body" + }, + { + "id": "expiry-col", + "component": "Column", + "children": ["expiry-label", "expiry-date"], + "align": "end" + }, + { + "id": "expiry-label", + "component": "Text", + "text": "EXPIRES", + "variant": "caption" + }, + { + "id": "expiry-date", + "component": "Text", + "text": { + "path": "/expiryDate" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-credit-card", + "value": { + "cardType": "VISA", + "cardNumber": "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 4242", + "holderName": "SARAH JOHNSON", + "expiryDate": "09/27" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/23_step-counter.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/23_step-counter.json new file mode 100644 index 0000000000..a3cc2a1e3f --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/23_step-counter.json @@ -0,0 +1,149 @@ +{ + "name": "Step Counter", + "description": "Example of step counter demonstrating number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-step-counter", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-step-counter", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "steps-display", "goal-text", "divider", "stats-row"], + "align": "center" + }, + { + "id": "header", + "component": "Row", + "children": ["steps-icon", "title"], + "align": "center" + }, + { + "id": "steps-icon", + "component": "Icon", + "name": "person" + }, + { + "id": "title", + "component": "Text", + "text": "Today's Steps", + "variant": "h4" + }, + { + "id": "steps-display", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/steps" + } + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "goal-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/progress}% of ${formatNumber(value: ${/goal})} goal" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "stats-row", + "component": "Row", + "children": ["distance-col", "calories-col"], + "justify": "spaceAround" + }, + { + "id": "distance-col", + "component": "Column", + "children": ["distance-value", "distance-label"], + "align": "center" + }, + { + "id": "distance-value", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "${/distance} mi" + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "distance-label", + "component": "Text", + "text": "Distance", + "variant": "caption" + }, + { + "id": "calories-col", + "component": "Column", + "children": ["calories-value", "calories-label"], + "align": "center" + }, + { + "id": "calories-value", + "component": "Text", + "text": { + "call": "formatNumber", + "args": { + "value": { + "path": "/calories" + } + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "calories-label", + "component": "Text", + "text": "Calories", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-step-counter", + "value": { + "steps": 8432, + "goal": 10000, + "progress": 84, + "distance": 3.8, + "calories": 312 + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/24_recipe-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/24_recipe-card.json new file mode 100644 index 0000000000..5d19c71508 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/24_recipe-card.json @@ -0,0 +1,204 @@ +{ + "name": "Recipe Card", + "description": "Example of recipe card demonstrating Tabs and pluralization.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-recipe-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-recipe-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "tabs-container" + }, + { + "id": "tabs-container", + "component": "Tabs", + "tabs": [ + { + "title": "Overview", + "child": "overview-col" + }, + { + "title": "Ingredients", + "child": "ingredients-list" + }, + { + "title": "Instructions", + "child": "instructions-list" + } + ] + }, + { + "id": "overview-col", + "component": "Column", + "children": ["recipe-image", "overview-content"] + }, + { + "id": "recipe-image", + "component": "Image", + "url": { + "path": "/image" + }, + "fit": "cover" + }, + { + "id": "overview-content", + "component": "Column", + "children": ["title", "rating-row", "times-row", "servings"] + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating", "review-count"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "review-count", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "(${formatNumber(value: ${/reviewCount})} ${pluralize(value: ${/reviewCount}, one: 'review', other: 'reviews')})" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "times-row", + "component": "Row", + "children": ["prep-time", "cook-time"] + }, + { + "id": "prep-time", + "component": "Row", + "children": ["prep-icon", "prep-text"], + "align": "center" + }, + { + "id": "prep-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "prep-text", + "component": "Text", + "text": { + "path": "/prepTime" + }, + "variant": "caption" + }, + { + "id": "cook-time", + "component": "Row", + "children": ["cook-icon", "cook-text"], + "align": "center" + }, + { + "id": "cook-icon", + "component": "Icon", + "name": "warning" + }, + { + "id": "cook-text", + "component": "Text", + "text": { + "path": "/cookTime" + }, + "variant": "caption" + }, + { + "id": "servings", + "component": "Text", + "text": { + "path": "/servings" + }, + "variant": "caption" + }, + { + "id": "ingredients-list", + "component": "Column", + "children": { + "path": "/ingredients", + "componentId": "item-template" + } + }, + { + "id": "instructions-list", + "component": "Column", + "children": { + "path": "/instructions", + "componentId": "item-template" + } + }, + { + "id": "item-template", + "component": "Text", + "text": { + "path": "text" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-recipe-card", + "value": { + "image": "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=300&h=180&fit=crop", + "title": "Mediterranean Quinoa Bowl", + "rating": "4.9", + "reviewCount": 1247, + "prepTime": "15 min prep", + "cookTime": "20 min cook", + "servings": "Serves 4", + "ingredients": [ + {"text": "1 cup quinoa"}, + {"text": "2 cups water"}, + {"text": "1 cucumber, diced"}, + {"text": "1 cup cherry tomatoes, halved"} + ], + "instructions": [ + {"text": "1. Rinse quinoa and bring to a boil in water."}, + {"text": "2. Reduce heat and simmer for 15 minutes."}, + {"text": "3. Fluff with a fork and let cool."}, + {"text": "4. Mix with diced vegetables."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/25_contact-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/25_contact-card.json new file mode 100644 index 0000000000..ed00ec6e24 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/25_contact-card.json @@ -0,0 +1,175 @@ +{ + "name": "Contact Card", + "description": "Example of contact card", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-contact-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-contact-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["avatar-image", "name", "title", "divider", "contact-info", "actions"], + "align": "center" + }, + { + "id": "avatar-image", + "component": "Image", + "url": { + "path": "/avatar" + }, + "fit": "cover", + "variant": "avatar" + }, + { + "id": "name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "body" + }, + { + "id": "divider", + "component": "Divider" + }, + { + "id": "contact-info", + "component": "Column", + "children": ["phone-row", "email-row", "location-row"] + }, + { + "id": "phone-row", + "component": "Row", + "children": ["phone-icon", "phone-text"], + "align": "center" + }, + { + "id": "phone-icon", + "component": "Icon", + "name": "phone" + }, + { + "id": "phone-text", + "component": "Text", + "text": { + "path": "/phone" + }, + "variant": "body" + }, + { + "id": "email-row", + "component": "Row", + "children": ["email-icon", "email-text"], + "align": "center" + }, + { + "id": "email-icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "email-text", + "component": "Text", + "text": { + "path": "/email" + }, + "variant": "body" + }, + { + "id": "location-row", + "component": "Row", + "children": ["location-icon", "location-text"], + "align": "center" + }, + { + "id": "location-icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location-text", + "component": "Text", + "text": { + "path": "/location" + }, + "variant": "body" + }, + { + "id": "actions", + "component": "Row", + "children": ["call-btn", "message-btn"] + }, + { + "id": "call-btn-text", + "component": "Text", + "text": "Call" + }, + { + "id": "call-btn", + "component": "Button", + "child": "call-btn-text", + "action": { + "event": { + "name": "call", + "context": {} + } + } + }, + { + "id": "message-btn-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-btn", + "component": "Button", + "child": "message-btn-text", + "action": { + "event": { + "name": "message", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-contact-card", + "value": { + "avatar": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop", + "name": "David Park", + "title": "Engineering Manager", + "phone": "+1 (555) 234-5678", + "email": "david.park@company.com", + "location": "San Francisco, CA" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/26_podcast-episode.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/26_podcast-episode.json new file mode 100644 index 0000000000..02c1e7b089 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/26_podcast-episode.json @@ -0,0 +1,123 @@ +{ + "name": "Podcast Episode", + "description": "Example of podcast episode demonstrating AudioPlayer and date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-podcast-episode", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-podcast-episode", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-row" + }, + { + "id": "main-row", + "component": "Row", + "children": ["artwork", "content"], + "align": "start" + }, + { + "id": "artwork", + "component": "Image", + "url": { + "path": "/artwork" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["show-name", "episode-title", "meta-row", "description", "audio-player"] + }, + { + "id": "show-name", + "component": "Text", + "text": { + "path": "/showName" + }, + "variant": "caption" + }, + { + "id": "episode-title", + "component": "Text", + "text": { + "path": "/episodeTitle" + }, + "variant": "h4" + }, + { + "id": "meta-row", + "component": "Row", + "children": ["duration", "date"] + }, + { + "id": "duration", + "component": "Text", + "text": { + "path": "/duration" + }, + "variant": "caption" + }, + { + "id": "date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/date" + }, + "format": "MMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "caption" + }, + { + "id": "description", + "component": "Text", + "text": { + "path": "/description" + }, + "variant": "body" + }, + { + "id": "audio-player", + "component": "AudioPlayer", + "url": { + "path": "/audioUrl" + }, + "description": { + "path": "/episodeTitle" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-podcast-episode", + "value": { + "artwork": "https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=100&h=100&fit=crop", + "showName": "Tech Talk Daily", + "episodeTitle": "The Future of AI in Product Design", + "duration": "45 min", + "date": "2024-12-15", + "description": "How AI is transforming the way we design and build products.", + "audioUrl": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/27_stats-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/27_stats-card.json new file mode 100644 index 0000000000..aa1c1cdee9 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/27_stats-card.json @@ -0,0 +1,106 @@ +{ + "name": "Stats Card", + "description": "Example of stats card demonstrating currency and number formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-stats-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-stats-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header", "value", "trend-row"] + }, + { + "id": "header", + "component": "Row", + "children": ["metric-icon", "metric-name"], + "align": "center" + }, + { + "id": "metric-icon", + "component": "Icon", + "name": { + "path": "/icon" + } + }, + { + "id": "metric-name", + "component": "Text", + "text": { + "path": "/metricName" + }, + "variant": "caption" + }, + { + "id": "value", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": { + "value": { + "path": "/value" + }, + "currency": "USD" + }, + "returnType": "string" + }, + "variant": "h1" + }, + { + "id": "trend-row", + "component": "Row", + "children": ["trend-icon", "trend-text"], + "align": "center" + }, + { + "id": "trend-icon", + "component": "Icon", + "name": { + "path": "/trendIcon" + } + }, + { + "id": "trend-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "+${/trendPercent}% from last month" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-stats-card", + "value": { + "icon": "trending_up", + "metricName": "Monthly Revenue", + "value": 48294, + "trendIcon": "arrow_upward", + "trendPercent": 12.5 + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/28_countdown-timer.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/28_countdown-timer.json new file mode 100644 index 0000000000..5032bba156 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/28_countdown-timer.json @@ -0,0 +1,135 @@ +{ + "name": "Countdown Timer", + "description": "Example of countdown timer demonstrating date formatting.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-countdown-timer", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-countdown-timer", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["event-name", "countdown-row", "target-date"], + "align": "center" + }, + { + "id": "event-name", + "component": "Text", + "text": { + "path": "/eventName" + }, + "variant": "h3" + }, + { + "id": "countdown-row", + "component": "Row", + "children": ["days-col", "hours-col", "minutes-col"], + "justify": "spaceAround" + }, + { + "id": "days-col", + "component": "Column", + "children": ["days-value", "days-label"], + "align": "center" + }, + { + "id": "days-value", + "component": "Text", + "text": { + "path": "/days" + }, + "variant": "h1" + }, + { + "id": "days-label", + "component": "Text", + "text": "Days", + "variant": "caption" + }, + { + "id": "hours-col", + "component": "Column", + "children": ["hours-value", "hours-label"], + "align": "center" + }, + { + "id": "hours-value", + "component": "Text", + "text": { + "path": "/hours" + }, + "variant": "h1" + }, + { + "id": "hours-label", + "component": "Text", + "text": "Hours", + "variant": "caption" + }, + { + "id": "minutes-col", + "component": "Column", + "children": ["minutes-value", "minutes-label"], + "align": "center" + }, + { + "id": "minutes-value", + "component": "Text", + "text": { + "path": "/minutes" + }, + "variant": "h1" + }, + { + "id": "minutes-label", + "component": "Text", + "text": "Minutes", + "variant": "caption" + }, + { + "id": "target-date", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/targetDate" + }, + "format": "MMMM d, yyyy" + }, + "returnType": "string" + }, + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-countdown-timer", + "value": { + "eventName": "Product Launch", + "days": "14", + "hours": "08", + "minutes": "32", + "targetDate": "2025-01-15" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/29_movie-card.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/29_movie-card.json new file mode 100644 index 0000000000..0d9bdb553c --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/29_movie-card.json @@ -0,0 +1,156 @@ +{ + "name": "Movie Card", + "description": "Example of movie card demonstrating Modal and Video components.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-movie-card", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-movie-card", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["poster", "content", "trailer-modal"] + }, + { + "id": "poster", + "component": "Image", + "url": { + "path": "/poster" + }, + "fit": "cover" + }, + { + "id": "content", + "component": "Column", + "children": ["title-row", "genre", "rating-row", "runtime", "watch-trailer-btn"] + }, + { + "id": "title-row", + "component": "Row", + "children": ["movie-title", "year"], + "align": "start" + }, + { + "id": "movie-title", + "component": "Text", + "text": { + "path": "/title" + }, + "variant": "h3" + }, + { + "id": "year", + "component": "Text", + "text": { + "path": "/year" + }, + "variant": "caption" + }, + { + "id": "genre", + "component": "Text", + "text": { + "path": "/genre" + }, + "variant": "caption" + }, + { + "id": "rating-row", + "component": "Row", + "children": ["star-icon", "rating-value"], + "align": "center" + }, + { + "id": "star-icon", + "component": "Icon", + "name": "star" + }, + { + "id": "rating-value", + "component": "Text", + "text": { + "path": "/rating" + }, + "variant": "body" + }, + { + "id": "runtime", + "component": "Row", + "children": ["time-icon", "runtime-text"], + "align": "center" + }, + { + "id": "time-icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "runtime-text", + "component": "Text", + "text": { + "path": "/runtime" + }, + "variant": "caption" + }, + { + "id": "watch-trailer-btn-text", + "component": "Text", + "text": "Watch Trailer" + }, + { + "id": "watch-trailer-btn", + "component": "Button", + "child": "watch-trailer-btn-text", + "action": { + "event": { + "name": "open_trailer" + } + } + }, + { + "id": "trailer-modal", + "component": "Modal", + "trigger": "watch-trailer-btn", + "content": "trailer-video" + }, + { + "id": "trailer-video", + "component": "Video", + "url": { + "path": "/trailerUrl" + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-movie-card", + "value": { + "poster": "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=200&h=300&fit=crop", + "title": "Interstellar", + "year": "(2014)", + "genre": "Sci-Fi • Adventure • Drama", + "rating": "8.7/10", + "runtime": "2h 49min", + "trailerUrl": "https://www.w3schools.com/html/mov_bbb.mp4" + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/30_live-invitation-builder.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/30_live-invitation-builder.json new file mode 100644 index 0000000000..7ed5bc9f06 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/30_live-invitation-builder.json @@ -0,0 +1,205 @@ +{ + "name": "Live Invitation Builder", + "description": "Demonstrates reactive two-way binding where editor inputs update a live preview in real-time.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-invitation-builder", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-invitation-builder", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "main-content"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "# Invitation Builder", + "variant": "h1" + }, + { + "id": "main-content", + "component": "Row", + "children": ["editor-col", "preview-col"], + "align": "start" + }, + { + "id": "editor-col", + "component": "Column", + "children": [ + "editor-title", + "event-name-input", + "guest-input", + "date-input", + "location-picker" + ], + "weight": 1, + "align": "stretch" + }, + { + "id": "editor-title", + "component": "Text", + "text": "Customize your invitation", + "variant": "h3" + }, + { + "id": "event-name-input", + "component": "TextField", + "label": "Event Name", + "value": { + "path": "/event/name" + } + }, + { + "id": "guest-input", + "component": "TextField", + "label": "Guest of Honor", + "value": { + "path": "/event/guest" + } + }, + { + "id": "date-input", + "component": "DateTimeInput", + "label": "Event Date & Time", + "value": { + "path": "/event/date" + }, + "enableDate": true, + "enableTime": true + }, + { + "id": "location-picker", + "component": "ChoicePicker", + "label": "Location", + "options": [ + {"label": "Grand Ballroom", "value": "ballroom"}, + {"label": "Sunset Terrace", "value": "terrace"}, + {"label": "Garden Pavillion", "value": "garden"} + ], + "value": { + "path": "/event/location" + }, + "variant": "mutuallyExclusive", + "displayStyle": "chips" + }, + { + "id": "preview-col", + "component": "Column", + "children": ["preview-title", "invitation-card"], + "weight": 1, + "align": "center" + }, + { + "id": "preview-title", + "component": "Text", + "text": "Live Preview", + "variant": "caption" + }, + { + "id": "invitation-card", + "component": "Card", + "child": "invitation-content" + }, + { + "id": "invitation-content", + "component": "Column", + "children": [ + "invite-image", + "invite-event-name", + "invite-guest-row", + "invite-date-text", + "invite-location-text" + ], + "align": "center" + }, + { + "id": "invite-image", + "component": "Image", + "url": "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=400&h=200&fit=crop", + "variant": "mediumFeature" + }, + { + "id": "invite-event-name", + "component": "Text", + "text": { + "path": "/event/name" + }, + "variant": "h2" + }, + { + "id": "invite-guest-row", + "component": "Row", + "children": ["invite-for-text", "invite-guest-name"], + "align": "center" + }, + { + "id": "invite-for-text", + "component": "Text", + "text": "Celebrating", + "variant": "body" + }, + { + "id": "invite-guest-name", + "component": "Text", + "text": { + "path": "/event/guest" + }, + "variant": "h3" + }, + { + "id": "invite-date-text", + "component": "Text", + "text": { + "call": "formatDate", + "args": { + "value": { + "path": "/event/date" + }, + "format": "EEEE, MMMM d, yyyy 'at' h:mm a" + }, + "returnType": "string" + }, + "variant": "body" + }, + { + "id": "invite-location-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Location: ${/event/location/0}" + }, + "returnType": "string" + }, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-invitation-builder", + "value": { + "event": { + "name": "Summer Gala", + "guest": "Alex Johnson", + "date": "2025-07-15T19:00:00Z", + "location": ["terrace"] + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/31_incremental-dashboard.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/31_incremental-dashboard.json new file mode 100644 index 0000000000..c05ed4f312 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/31_incremental-dashboard.json @@ -0,0 +1,128 @@ +{ + "name": "Incremental Dashboard", + "description": "Demonstrates structural evolution of a UI where loading placeholders are incrementally replaced by actual components.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-incremental-dashboard", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json" + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["header", "content-grid"], + "align": "stretch" + }, + { + "id": "header", + "component": "Text", + "text": "System Dashboard", + "variant": "h2" + }, + { + "id": "content-grid", + "component": "Row", + "children": ["left-panel", "right-panel"] + }, + { + "id": "left-panel", + "component": "Column", + "children": ["panel-a-loading"], + "weight": 1 + }, + { + "id": "right-panel", + "component": "Column", + "children": ["panel-b-loading"], + "weight": 1 + }, + { + "id": "panel-a-loading", + "component": "Text", + "text": "Loading analytics...", + "variant": "caption" + }, + { + "id": "panel-b-loading", + "component": "Text", + "text": "Loading logs...", + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "left-panel", + "component": "Column", + "children": ["analytics-card"], + "weight": 1 + }, + { + "id": "analytics-card", + "component": "Card", + "child": "analytics-text" + }, + { + "id": "analytics-text", + "component": "Text", + "text": "Analytics are ready.", + "variant": "body" + } + ] + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-incremental-dashboard", + "components": [ + { + "id": "right-panel", + "component": "Column", + "children": ["logs-list"], + "weight": 1 + }, + { + "id": "logs-list", + "component": "List", + "children": { + "path": "/logs", + "componentId": "log-template" + } + }, + { + "id": "log-template", + "component": "Text", + "text": {"path": "message"}, + "variant": "caption" + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-incremental-dashboard", + "value": { + "logs": [ + {"message": "System boot complete."}, + {"message": "All services healthy."}, + {"message": "Waiting for user input."} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/32_advanced-form-validator.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/32_advanced-form-validator.json new file mode 100644 index 0000000000..13a0b076e6 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/32_advanced-form-validator.json @@ -0,0 +1,166 @@ +{ + "name": "Advanced Form Validator", + "description": "Demonstrates complex validation logic and deeply nested formatting functions.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-advanced-validator", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-advanced-validator", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "welcome-text", + "email-field", + "phone-field", + "zip-field", + "terms-checkbox", + "submit-btn" + ], + "align": "stretch" + }, + { + "id": "welcome-text", + "component": "Text", + "text": { + "call": "formatString", + "args": { + "value": "Hello! Today is ${formatDate(value: ${/now}, format: 'EEEE, MMMM d')}." + }, + "returnType": "string" + }, + "variant": "h3" + }, + { + "id": "email-field", + "component": "TextField", + "label": "Email Address", + "value": {"path": "/formData/email"}, + "checks": [ + { + "condition": { + "call": "email", + "args": {"value": {"path": "/formData/email"}} + }, + "message": "Invalid email format" + } + ] + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone Number", + "value": {"path": "/formData/phone"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": { + "value": {"path": "/formData/phone"}, + "pattern": "^\\+?[0-9]{10,15}$" + } + }, + "message": "Invalid phone format" + } + ] + }, + { + "id": "zip-field", + "component": "TextField", + "label": "Zip Code", + "value": {"path": "/formData/zip"}, + "checks": [ + { + "condition": { + "call": "regex", + "args": {"value": {"path": "/formData/zip"}, "pattern": "^[0-9]{5}$"} + }, + "message": "Must be exactly 5 digits" + } + ] + }, + { + "id": "terms-checkbox", + "component": "CheckBox", + "label": "I agree to the terms and conditions", + "value": {"path": "/formData/agree"} + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit Registration" + }, + { + "id": "submit-btn", + "component": "Button", + "child": "submit-btn-text", + "checks": [ + { + "condition": { + "call": "and", + "args": { + "values": [ + {"path": "/formData/agree"}, + { + "call": "or", + "args": { + "values": [ + { + "call": "required", + "args": {"value": {"path": "/formData/email"}} + }, + { + "call": "required", + "args": {"value": {"path": "/formData/phone"}} + } + ] + } + }, + {"call": "required", "args": {"value": {"path": "/formData/zip"}}} + ] + } + }, + "message": "You must agree to terms AND provide either Email or Phone, plus a Zip code." + } + ], + "action": { + "event": { + "name": "register", + "context": {"data": {"path": "/formData"}} + } + } + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-advanced-validator", + "value": { + "now": "2025-12-15T12:00:00Z", + "formData": { + "email": "", + "phone": "", + "zip": "", + "agree": false + } + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/33_financial-data-grid.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/33_financial-data-grid.json new file mode 100644 index 0000000000..5113d87883 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/33_financial-data-grid.json @@ -0,0 +1,171 @@ +{ + "name": "Financial Data Grid", + "description": "Demonstrates complex layout weighting and alignment using Rows and Columns with templates.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-financial-grid", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-financial-grid", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["header-row", "grid-list"], + "align": "stretch" + }, + { + "id": "header-row", + "component": "Row", + "children": ["col-asset", "col-price", "col-change", "col-market-cap"], + "align": "center" + }, + { + "id": "col-asset", + "component": "Text", + "text": "Asset", + "variant": "caption", + "weight": 2 + }, + { + "id": "col-price", + "component": "Text", + "text": "Price", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-change", + "component": "Text", + "text": "24h Change", + "variant": "caption", + "weight": 1 + }, + { + "id": "col-market-cap", + "component": "Text", + "text": "Market Cap", + "variant": "caption", + "weight": 1.5 + }, + { + "id": "grid-list", + "component": "List", + "children": { + "path": "/assets", + "componentId": "row-template" + } + }, + { + "id": "row-template", + "component": "Row", + "children": ["asset-info", "asset-price", "asset-change", "asset-market-cap"], + "align": "center" + }, + { + "id": "asset-info", + "component": "Row", + "children": ["asset-icon", "asset-name-col"], + "weight": 2, + "align": "center" + }, + { + "id": "asset-icon", + "component": "Icon", + "name": "payment" + }, + { + "id": "asset-name-col", + "component": "Column", + "children": ["asset-name", "asset-symbol"] + }, + { + "id": "asset-name", + "component": "Text", + "text": {"path": "name"}, + "variant": "body" + }, + { + "id": "asset-symbol", + "component": "Text", + "text": {"path": "symbol"}, + "variant": "caption" + }, + { + "id": "asset-price", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "price"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-change", + "component": "Text", + "text": { + "call": "formatString", + "args": {"value": "${change}%"}, + "returnType": "string" + }, + "weight": 1 + }, + { + "id": "asset-market-cap", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": {"value": {"path": "marketCap"}, "currency": "USD"}, + "returnType": "string" + }, + "weight": 1.5 + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-financial-grid", + "value": { + "assets": [ + { + "name": "Bitcoin", + "symbol": "BTC", + "price": 43500.25, + "change": 1.2, + "marketCap": 850000000000 + }, + { + "name": "Ethereum", + "symbol": "ETH", + "price": 2250.5, + "change": -0.5, + "marketCap": 270000000000 + }, + { + "name": "Solana", + "symbol": "SOL", + "price": 95.8, + "change": 5.4, + "marketCap": 40000000000 + } + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/34_child-list-template.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/34_child-list-template.json new file mode 100644 index 0000000000..adb9196e68 --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/34_child-list-template.json @@ -0,0 +1,80 @@ +{ + "name": "ChildList Template Expansion", + "description": "Demonstrates dynamic list generation using ChildList object templates.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-child-list-template", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-child-list-template", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "item-list"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Dynamic Item List", + "variant": "h3" + }, + { + "id": "item-list", + "component": "List", + "children": { + "componentId": "item-row", + "path": "/items" + } + }, + { + "id": "item-row", + "component": "Row", + "children": ["item-name", "qty-label", "item-qty"] + }, + { + "id": "item-name", + "component": "Text", + "text": {"path": "name"} + }, + { + "id": "qty-label", + "component": "Text", + "text": " - Qty: " + }, + { + "id": "item-qty", + "component": "Text", + "text": {"path": "quantity"} + } + ] + } + }, + { + "version": "v0.10", + "updateDataModel": { + "surfaceId": "gallery-child-list-template", + "value": { + "items": [ + {"name": "Apple", "quantity": 10}, + {"name": "Banana", "quantity": 5}, + {"name": "Cherry", "quantity": 20} + ] + } + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/35_markdown-text.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/35_markdown-text.json new file mode 100644 index 0000000000..036b75f01b --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/35_markdown-text.json @@ -0,0 +1,44 @@ +{ + "name": "Markdown Text Support", + "description": "Demonstrates Markdown rendering in Text component.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "gallery-markdown-text", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "gallery-markdown-text", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": ["title-text", "markdown-content"], + "align": "stretch" + }, + { + "id": "title-text", + "component": "Text", + "text": "Markdown Rendering", + "variant": "h3" + }, + { + "id": "markdown-content", + "component": "Text", + "text": "# Heading 1\n\nThis is **bold** text and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link to Google](https://google.com)" + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/36_modal.json b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/36_modal.json new file mode 100644 index 0000000000..84902f57df --- /dev/null +++ b/agent_sdks/go/a2uischema/testdata/v0_10/basic/examples/36_modal.json @@ -0,0 +1,65 @@ +{ + "name": "Modal Sample", + "description": "Example of Modal component showing a trigger and content.", + "messages": [ + { + "version": "v0.10", + "createSurface": { + "surfaceId": "modal-sample-surface", + "catalogId": "https://a2ui.org/specification/v0_10/catalogs/basic/catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.10", + "updateComponents": { + "surfaceId": "modal-sample-surface", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["title", "modal-comp"] + }, + { + "id": "title", + "component": "Text", + "text": "Modal Component Sample", + "variant": "h2" + }, + { + "id": "modal-comp", + "component": "Modal", + "trigger": "open-btn", + "content": "modal-content" + }, + { + "id": "open-btn-text", + "component": "Text", + "text": "Open Modal" + }, + { + "id": "open-btn", + "component": "Button", + "child": "open-btn-text", + "action": { + "event": { + "name": "openModalEvent", + "context": {} + } + } + }, + { + "id": "modal-content", + "component": "Column", + "children": ["modal-text"] + }, + { + "id": "modal-text", + "component": "Text", + "text": "This is the content inside the modal." + } + ] + } + } + ] +} diff --git a/agent_sdks/go/a2uischema/validator.go b/agent_sdks/go/a2uischema/validator.go new file mode 100644 index 0000000000..2e7b899903 --- /dev/null +++ b/agent_sdks/go/a2uischema/validator.go @@ -0,0 +1,687 @@ +package a2uischema + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "slices" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + a2uiv010 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010" +) + +var ( + jsonPointerPattern = regexp.MustCompile(`^(?:/(?:[^~/]|~[01])*)*$`) + relativeJSONPointerPattern = regexp.MustCompile(`^(?:[^~/]|~[01])+(?:/(?:[^~/]|~[01])*)*$`) +) + +// Validator validates A2UI payloads against the selected catalog and protocol rules. +type Validator struct { + catalog *Catalog + allowedComponents map[string]struct{} + allowedFunctions map[string]struct{} +} + +// NewValidator constructs a validator for a catalog. +func NewValidator(catalog *Catalog) *Validator { + v := &Validator{ + catalog: catalog, + allowedComponents: make(map[string]struct{}), + allowedFunctions: make(map[string]struct{}), + } + if catalog == nil { + return v + } + if components, ok := catalog.CatalogSchema[CatalogComponentsKey].(map[string]any); ok { + for name := range components { + v.allowedComponents[name] = struct{}{} + } + } + switch functions := catalog.CatalogSchema[CatalogFunctionsKey].(type) { + case map[string]any: + for name := range functions { + v.allowedFunctions[name] = struct{}{} + } + case []any: + for _, item := range functions { + name, _ := item.(map[string]any)["name"].(string) + if name != "" { + v.allowedFunctions[name] = struct{}{} + } + } + } + return v +} + +// ParseMessages parses a single message object or an array of messages. +func (v *Validator) ParseMessages(data []byte) ([]a2ui.ServerMessage, error) { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return nil, fmt.Errorf("schema: empty payload") + } + if data[0] == '[' { + var msgs []a2ui.ServerMessage + if err := json.Unmarshal(data, &msgs); err != nil { + return nil, fmt.Errorf("schema: parse messages: %w", err) + } + return msgs, nil + } + var msg a2ui.ServerMessage + if err := json.Unmarshal(data, &msg); err != nil { + return nil, fmt.Errorf("schema: parse message: %w", err) + } + return []a2ui.ServerMessage{msg}, nil +} + +// ValidateJSON parses and validates a raw JSON payload. +func (v *Validator) ValidateJSON(data []byte) error { + if v.catalog != nil && v.catalog.Version == Version010 { + msgs, err := v.parseMessagesV010(data) + if err != nil { + return err + } + return v.validateMessagesV010(msgs) + } + msgs, err := v.ParseMessages(data) + if err != nil { + return err + } + return v.ValidateMessages(msgs) +} + +// ValidateExample validates either a raw message payload or an example file +// with a top-level messages array. +func (v *Validator) ValidateExample(data []byte) error { + err := v.ValidateJSON(data) + if err == nil { + return nil + } + var example struct { + Messages json.RawMessage `json:"messages"` + } + if json.Unmarshal(data, &example) != nil || len(bytes.TrimSpace(example.Messages)) == 0 { + return err + } + return v.ValidateJSON(example.Messages) +} + +// ValidateVersionMessages validates a batch of A2UI messages for any supported version. +func (v *Validator) ValidateVersionMessages(msgs any) error { + switch msgs := msgs.(type) { + case []a2ui.ServerMessage: + return v.ValidateMessages(msgs) + case []a2uiv010.ServerMessage: + return v.validateMessagesV010(msgs) + default: + return fmt.Errorf("schema: unsupported messages type %T", msgs) + } +} + +// ValidateMessages validates a batch of A2UI v0.9 messages. +func (v *Validator) ValidateMessages(msgs []a2ui.ServerMessage) error { + if len(msgs) == 0 { + return fmt.Errorf("schema: no messages to validate") + } + surfaces := make(map[string]string) + surfaceComponents := make(map[string]map[string]bool) + for i, msg := range msgs { + if err := v.validateMessage(msg); err != nil { + return fmt.Errorf("schema: message[%d]: %w", i, err) + } + switch { + case msg.CreateSurface != nil: + surfaces[msg.CreateSurface.SurfaceID] = msg.CreateSurface.CatalogID + if surfaceComponents[msg.CreateSurface.SurfaceID] == nil { + surfaceComponents[msg.CreateSurface.SurfaceID] = make(map[string]bool) + } + case msg.UpdateComponents != nil: + if catalogID, ok := surfaces[msg.UpdateComponents.SurfaceID]; ok { + _ = catalogID + } + known := surfaceComponents[msg.UpdateComponents.SurfaceID] + if err := v.validateComponents(msg.UpdateComponents.Components, known); err != nil { + return fmt.Errorf("updateComponents: %w", err) + } + if known == nil { + known = make(map[string]bool) + surfaceComponents[msg.UpdateComponents.SurfaceID] = known + } + for _, component := range msg.UpdateComponents.Components { + known[component.ID] = true + } + case msg.UpdateDataModel != nil: + if err := validatePath(msg.UpdateDataModel.Path, true); err != nil { + return fmt.Errorf("updateDataModel.path: %w", err) + } + } + } + return nil +} + +func (v *Validator) validateMessage(msg a2ui.ServerMessage) error { + wantVersion := wireVersion(v.catalog.Version) + if msg.Version != string(wantVersion) { + return fmt.Errorf("version = %q, want %q", msg.Version, wantVersion) + } + switch { + case msg.CreateSurface != nil: + if msg.CreateSurface.SurfaceID == "" { + return fmt.Errorf("createSurface.surfaceId is required") + } + if msg.CreateSurface.CatalogID == "" { + return fmt.Errorf("createSurface.catalogId is required") + } + if v.catalog != nil { + id, err := v.catalog.ID() + if err == nil && len(v.allowedComponents) > 0 && msg.CreateSurface.CatalogID != id { + return fmt.Errorf("createSurface.catalogId = %q, want %q", msg.CreateSurface.CatalogID, id) + } + } + case msg.UpdateComponents != nil: + if msg.UpdateComponents.SurfaceID == "" { + return fmt.Errorf("updateComponents.surfaceId is required") + } + if len(msg.UpdateComponents.Components) == 0 { + return fmt.Errorf("updateComponents.components must not be empty") + } + case msg.UpdateDataModel != nil: + if msg.UpdateDataModel.SurfaceID == "" { + return fmt.Errorf("updateDataModel.surfaceId is required") + } + case msg.DeleteSurface != nil: + if msg.DeleteSurface.SurfaceID == "" { + return fmt.Errorf("deleteSurface.surfaceId is required") + } + default: + return fmt.Errorf("message has no payload") + } + return nil +} + +func isV09WireVersion(version Version) bool { + return version == Version09 || version == Version091 +} + +func wireVersion(version Version) Version { + if version == Version091 { + return Version09 + } + return version +} + +func (v *Validator) validateComponents(components []a2ui.Component, known map[string]bool) error { + ids := make(map[string]int, len(components)) + for i, component := range components { + if err := v.validateComponent(component); err != nil { + return fmt.Errorf("component[%d] (%s): %w", i, component.ID, err) + } + if _, ok := ids[component.ID]; ok { + return validationError(ValidationDuplicateComponent, "", component.ID, "", "", fmt.Sprintf("duplicate component id %q", component.ID)) + } + ids[component.ID] = i + } + graph := make(map[string][]string, len(components)) + for _, component := range components { + refs, err := componentRefs(component) + if err != nil { + return fmt.Errorf("component %q: %w", component.ID, err) + } + graph[component.ID] = nil + for _, ref := range refs { + if _, ok := ids[ref]; ok { + graph[component.ID] = append(graph[component.ID], ref) + continue + } + if known != nil && known[ref] { + continue + } + return validationError(ValidationUnknownComponentRef, "", component.ID, ref, "", fmt.Sprintf("component %q references unknown component %q", component.ID, ref)) + } + } + if _, ok := ids["root"]; !ok && len(known) == 0 { + return validationError(ValidationMissingRootComponent, "", "root", "", "", fmt.Sprintf("components must include id %q", "root")) + } + if _, ok := ids["root"]; ok { + if err := validateTopology(graph, "root"); err != nil { + return err + } + } + return nil +} + +func (v *Validator) validateComponent(component a2ui.Component) error { + if component.ID == "" { + return fmt.Errorf("id is required") + } + componentType := component.ComponentType() + if componentType == "" { + return fmt.Errorf("exactly one concrete component type must be set") + } + if len(v.allowedComponents) > 0 { + if _, ok := v.allowedComponents[componentType]; !ok { + return validationError(ValidationUnknownComponentType, "", component.ID, "", "", fmt.Sprintf("component type %q is not allowed by the selected catalog", componentType)) + } + } + for _, check := range component.Checks { + if check.Message == "" { + return fmt.Errorf("check message is required") + } + if err := v.validateDynamicBoolean(check.Condition, 0); err != nil { + return fmt.Errorf("check condition: %w", err) + } + } + if component.Accessibility != nil { + if component.Accessibility.Label != nil { + if err := v.validateDynamicString(*component.Accessibility.Label, 0); err != nil { + return fmt.Errorf("accessibility.label: %w", err) + } + } + if component.Accessibility.Description != nil { + if err := v.validateDynamicString(*component.Accessibility.Description, 0); err != nil { + return fmt.Errorf("accessibility.description: %w", err) + } + } + } + switch { + case component.Text != nil: + return v.validateTextComponent(*component.Text) + case component.Image != nil: + return v.validateImageComponent(*component.Image) + case component.Icon != nil: + return v.validateIconComponent(*component.Icon) + case component.Video != nil: + return v.validateVideoComponent(*component.Video) + case component.AudioPlayer != nil: + return v.validateAudioPlayerComponent(*component.AudioPlayer) + case component.Row != nil: + return v.validateContainerChildren(component.Row.Children) + case component.Column != nil: + return v.validateContainerChildren(component.Column.Children) + case component.List != nil: + return v.validateContainerChildren(component.List.Children) + case component.Card != nil: + if component.Card.Child == "" { + return fmt.Errorf("card.child is required") + } + case component.Tabs != nil: + if len(component.Tabs.Tabs) == 0 { + return fmt.Errorf("tabs.tabs must not be empty") + } + for _, tab := range component.Tabs.Tabs { + if tab.Child == "" { + return fmt.Errorf("tabs.child is required") + } + if err := v.validateDynamicString(tab.Title, 0); err != nil { + return fmt.Errorf("tabs.title: %w", err) + } + } + case component.Modal != nil: + if component.Modal.Content == "" || component.Modal.Trigger == "" { + return fmt.Errorf("modal.content and modal.trigger are required") + } + case component.Divider != nil: + return nil + case component.Button != nil: + if component.Button.Child == "" { + return fmt.Errorf("button.child is required") + } + if err := v.validateAction(component.Button.Action, 0); err != nil { + return fmt.Errorf("button.action: %w", err) + } + case component.TextField != nil: + if err := v.validateDynamicString(component.TextField.Label, 0); err != nil { + return fmt.Errorf("textField.label: %w", err) + } + if component.TextField.Value != nil { + if err := v.validateDynamicString(*component.TextField.Value, 0); err != nil { + return fmt.Errorf("textField.value: %w", err) + } + } + case component.CheckBox != nil: + if err := v.validateDynamicString(component.CheckBox.Label, 0); err != nil { + return fmt.Errorf("checkBox.label: %w", err) + } + if err := v.validateDynamicBoolean(component.CheckBox.Value, 0); err != nil { + return fmt.Errorf("checkBox.value: %w", err) + } + case component.ChoicePicker != nil: + if len(component.ChoicePicker.Options) == 0 { + return fmt.Errorf("choicePicker.options must not be empty") + } + if component.ChoicePicker.Label != nil { + if err := v.validateDynamicString(*component.ChoicePicker.Label, 0); err != nil { + return fmt.Errorf("choicePicker.label: %w", err) + } + } + for _, option := range component.ChoicePicker.Options { + if option.Value == "" { + return fmt.Errorf("choicePicker option value is required") + } + if err := v.validateDynamicString(option.Label, 0); err != nil { + return fmt.Errorf("choicePicker option label: %w", err) + } + } + if err := v.validateDynamicStringList(component.ChoicePicker.Value, 0); err != nil { + return fmt.Errorf("choicePicker.value: %w", err) + } + case component.Slider != nil: + if err := v.validateDynamicNumber(component.Slider.Value, 0); err != nil { + return fmt.Errorf("slider.value: %w", err) + } + if component.Slider.Label != nil { + if err := v.validateDynamicString(*component.Slider.Label, 0); err != nil { + return fmt.Errorf("slider.label: %w", err) + } + } + case component.DateTimeInput != nil: + if err := v.validateDynamicString(component.DateTimeInput.Value, 0); err != nil { + return fmt.Errorf("dateTimeInput.value: %w", err) + } + for name, value := range map[string]*a2ui.DynamicString{ + "label": component.DateTimeInput.Label, + "max": component.DateTimeInput.Max, + "min": component.DateTimeInput.Min, + } { + if value != nil { + if err := v.validateDynamicString(*value, 0); err != nil { + return fmt.Errorf("dateTimeInput.%s: %w", name, err) + } + } + } + } + return nil +} + +func (v *Validator) validateTextComponent(component a2ui.TextComponent) error { + return v.validateDynamicString(component.Text, 0) +} + +func (v *Validator) validateImageComponent(component a2ui.ImageComponent) error { + if err := v.validateDynamicString(component.URL, 0); err != nil { + return err + } + if component.Description != nil { + if err := v.validateDynamicString(*component.Description, 0); err != nil { + return err + } + } + return nil +} + +func (v *Validator) validateIconComponent(component a2ui.IconComponent) error { + if component.Name.Name == nil && component.Name.SVGPath == nil && component.Name.Binding == nil { + return fmt.Errorf("icon.name is required") + } + return nil +} + +func (v *Validator) validateVideoComponent(component a2ui.VideoComponent) error { + return v.validateDynamicString(component.URL, 0) +} + +func (v *Validator) validateAudioPlayerComponent(component a2ui.AudioPlayerComponent) error { + if err := v.validateDynamicString(component.URL, 0); err != nil { + return err + } + if component.Description != nil { + return v.validateDynamicString(*component.Description, 0) + } + return nil +} + +func (v *Validator) validateContainerChildren(children a2ui.ChildList) error { + if len(children.IDs) == 0 && children.Template == nil { + return fmt.Errorf("children must not be empty") + } + if children.Template != nil { + if children.Template.ComponentID == "" { + return fmt.Errorf("children.template.componentId is required") + } + if err := validatePath(children.Template.Path, false); err != nil { + return fmt.Errorf("children.template.path: %w", err) + } + } + return nil +} + +func (v *Validator) validateAction(action a2ui.Action, depth int) error { + switch { + case action.Event != nil && action.FunctionCall != nil: + return fmt.Errorf("action must not have both event and functionCall") + case action.Event != nil: + if action.Event.Name == "" { + return fmt.Errorf("event.name is required") + } + for key, value := range action.Event.Context { + if err := v.validateDynamicValue(value, depth+1); err != nil { + return fmt.Errorf("event.context[%q]: %w", key, err) + } + } + case action.FunctionCall != nil: + if err := v.validateFunctionCall(*action.FunctionCall, depth+1); err != nil { + return err + } + default: + return fmt.Errorf("action must have event or functionCall") + } + return nil +} + +func (v *Validator) validateDynamicString(value a2ui.DynamicString, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCall(*value.FunctionCall, depth+1) + default: + return fmt.Errorf("dynamic string has no value") + } +} + +func (v *Validator) validateDynamicNumber(value a2ui.DynamicNumber, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCall(*value.FunctionCall, depth+1) + default: + return fmt.Errorf("dynamic number has no value") + } +} + +func (v *Validator) validateDynamicBoolean(value a2ui.DynamicBoolean, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCall(*value.FunctionCall, depth+1) + default: + return fmt.Errorf("dynamic boolean has no value") + } +} + +func (v *Validator) validateDynamicStringList(value a2ui.DynamicStringList, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCall(*value.FunctionCall, depth+1) + default: + return fmt.Errorf("dynamic string list has no value") + } +} + +func (v *Validator) validateDynamicValue(value a2ui.DynamicValue, depth int) error { + switch { + case value.String != nil, value.Number != nil, value.Bool != nil, value.Array != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCall(*value.FunctionCall, depth+1) + default: + return fmt.Errorf("dynamic value has no value") + } +} + +func (v *Validator) validateFunctionCall(call a2ui.FunctionCall, depth int) error { + if depth > 32 { + return fmt.Errorf("function call recursion depth exceeded") + } + if call.Call == "" { + return fmt.Errorf("function call name is required") + } + if len(v.allowedFunctions) > 0 { + if _, ok := v.allowedFunctions[call.Call]; !ok { + return validationError(ValidationUnknownFunction, "", "", "", call.Call, fmt.Sprintf("unknown function %q", call.Call)) + } + } + for key, arg := range call.Args { + if err := v.validateFunctionArg(arg, depth+1); err != nil { + return fmt.Errorf("function arg %q: %w", key, err) + } + } + return nil +} + +func (v *Validator) validateFunctionArg(arg any, depth int) error { + switch value := arg.(type) { + case nil, string, bool, float64, int: + return nil + case []string: + return nil + case []any: + for i, item := range value { + if err := v.validateFunctionArg(item, depth+1); err != nil { + return fmt.Errorf("[%d]: %w", i, err) + } + } + return nil + case map[string]any: + if _, ok := value["path"]; ok { + path, _ := value["path"].(string) + return validatePath(path, false) + } + if _, ok := value["call"]; ok { + data, err := json.Marshal(value) + if err != nil { + return err + } + var call a2ui.FunctionCall + if err := json.Unmarshal(data, &call); err != nil { + return err + } + return v.validateFunctionCall(call, depth+1) + } + keys := make([]string, 0, len(value)) + for key := range value { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + if err := v.validateFunctionArg(value[key], depth+1); err != nil { + return fmt.Errorf("%s: %w", key, err) + } + } + return nil + case a2ui.DynamicValue: + return v.validateDynamicValue(value, depth+1) + case a2ui.DynamicString: + return v.validateDynamicString(value, depth+1) + case a2ui.DynamicNumber: + return v.validateDynamicNumber(value, depth+1) + case a2ui.DynamicBoolean: + return v.validateDynamicBoolean(value, depth+1) + case a2ui.DynamicStringList: + return v.validateDynamicStringList(value, depth+1) + default: + return nil + } +} + +func componentRefs(component a2ui.Component) ([]string, error) { + switch { + case component.Button != nil: + return []string{component.Button.Child}, nil + case component.Card != nil: + return []string{component.Card.Child}, nil + case component.Column != nil: + return childListRefs(component.Column.Children) + case component.List != nil: + return childListRefs(component.List.Children) + case component.Row != nil: + return childListRefs(component.Row.Children) + case component.Modal != nil: + return []string{component.Modal.Trigger, component.Modal.Content}, nil + case component.Tabs != nil: + refs := make([]string, 0, len(component.Tabs.Tabs)) + for _, tab := range component.Tabs.Tabs { + refs = append(refs, tab.Child) + } + return refs, nil + default: + return nil, nil + } +} + +func childListRefs(children a2ui.ChildList) ([]string, error) { + if children.Template != nil { + return []string{children.Template.ComponentID}, nil + } + return append([]string(nil), children.IDs...), nil +} + +func validateTopology(graph map[string][]string, root string) error { + seen := make(map[string]bool, len(graph)) + stack := make(map[string]bool, len(graph)) + var visit func(string) error + visit = func(node string) error { + if stack[node] { + return validationError(ValidationCycle, "", node, "", "", fmt.Sprintf("cycle detected at component %q", node)) + } + if seen[node] { + return nil + } + seen[node] = true + stack[node] = true + for _, child := range graph[node] { + if err := visit(child); err != nil { + return err + } + } + delete(stack, node) + return nil + } + if err := visit(root); err != nil { + return err + } + if len(seen) != len(graph) { + return validationError(ValidationOrphanedComponent, "", "", "", "", "orphaned components detected") + } + return nil +} + +func validatePath(path string, allowEmpty bool) error { + if path == "" { + if allowEmpty { + return nil + } + return validationError(ValidationInvalidPath, "", "", "", "", "path is required") + } + if path == "/" { + return nil + } + if !jsonPointerPattern.MatchString(path) && !relativeJSONPointerPattern.MatchString(path) { + return validationError(ValidationInvalidPath, path, "", "", "", fmt.Sprintf("invalid JSON Pointer %q", path)) + } + return nil +} diff --git a/agent_sdks/go/a2uischema/validator_v010.go b/agent_sdks/go/a2uischema/validator_v010.go new file mode 100644 index 0000000000..1e16d5499e --- /dev/null +++ b/agent_sdks/go/a2uischema/validator_v010.go @@ -0,0 +1,619 @@ +package a2uischema + +import ( + "bytes" + "encoding/json" + "fmt" + "slices" + + a2uiv010 "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui/v010" +) + +func (v *Validator) parseMessagesV010(data []byte) ([]a2uiv010.ServerMessage, error) { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return nil, fmt.Errorf("schema: empty payload") + } + if data[0] == '[' { + var msgs []a2uiv010.ServerMessage + if err := json.Unmarshal(data, &msgs); err != nil { + return nil, fmt.Errorf("schema: parse messages: %w", err) + } + return msgs, nil + } + var msg a2uiv010.ServerMessage + if err := json.Unmarshal(data, &msg); err != nil { + return nil, fmt.Errorf("schema: parse message: %w", err) + } + return []a2uiv010.ServerMessage{msg}, nil +} + +func (v *Validator) validateMessagesV010(msgs []a2uiv010.ServerMessage) error { + if len(msgs) == 0 { + return fmt.Errorf("schema: no messages to validate") + } + surfaces := make(map[string]string) + surfaceComponents := make(map[string]map[string]bool) + for i, msg := range msgs { + if err := v.validateMessageV010(msg); err != nil { + return fmt.Errorf("schema: message[%d]: %w", i, err) + } + switch { + case msg.CreateSurface != nil: + surfaces[msg.CreateSurface.SurfaceID] = msg.CreateSurface.CatalogID + if surfaceComponents[msg.CreateSurface.SurfaceID] == nil { + surfaceComponents[msg.CreateSurface.SurfaceID] = make(map[string]bool) + } + case msg.UpdateComponents != nil: + if catalogID, ok := surfaces[msg.UpdateComponents.SurfaceID]; ok { + _ = catalogID + } + known := surfaceComponents[msg.UpdateComponents.SurfaceID] + if err := v.validateComponentsV010(msg.UpdateComponents.Components, known); err != nil { + return fmt.Errorf("updateComponents: %w", err) + } + if known == nil { + known = make(map[string]bool) + surfaceComponents[msg.UpdateComponents.SurfaceID] = known + } + for _, component := range msg.UpdateComponents.Components { + known[component.ID] = true + } + case msg.UpdateDataModel != nil: + if err := validatePath(msg.UpdateDataModel.Path, true); err != nil { + return fmt.Errorf("updateDataModel.path: %w", err) + } + } + } + return nil +} + +func (v *Validator) validateMessageV010(msg a2uiv010.ServerMessage) error { + wantVersion := Version010 + if v.catalog != nil { + wantVersion = v.catalog.Version + } + if msg.Version != string(wantVersion) { + return fmt.Errorf("version = %q, want %q", msg.Version, wantVersion) + } + if msg.FunctionCallID != "" && msg.CallFunction == nil { + return fmt.Errorf("functionCallId requires callFunction") + } + if msg.ActionID != "" && msg.ActionResponse == nil { + return fmt.Errorf("actionId requires actionResponse") + } + switch countSetV010(msg.CreateSurface != nil, msg.UpdateComponents != nil, msg.UpdateDataModel != nil, msg.DeleteSurface != nil, msg.CallFunction != nil, msg.ActionResponse != nil) { + case 1: + case 0: + return fmt.Errorf("message has no payload") + default: + return fmt.Errorf("message has multiple payloads") + } + switch { + case msg.CreateSurface != nil: + if msg.CreateSurface.SurfaceID == "" { + return fmt.Errorf("createSurface.surfaceId is required") + } + if msg.CreateSurface.CatalogID == "" { + return fmt.Errorf("createSurface.catalogId is required") + } + if v.catalog != nil { + id, err := v.catalog.ID() + if err == nil && len(v.allowedComponents) > 0 && msg.CreateSurface.CatalogID != id { + return fmt.Errorf("createSurface.catalogId = %q, want %q", msg.CreateSurface.CatalogID, id) + } + } + case msg.UpdateComponents != nil: + if msg.UpdateComponents.SurfaceID == "" { + return fmt.Errorf("updateComponents.surfaceId is required") + } + if len(msg.UpdateComponents.Components) == 0 { + return fmt.Errorf("updateComponents.components must not be empty") + } + case msg.UpdateDataModel != nil: + if msg.UpdateDataModel.SurfaceID == "" { + return fmt.Errorf("updateDataModel.surfaceId is required") + } + case msg.DeleteSurface != nil: + if msg.DeleteSurface.SurfaceID == "" { + return fmt.Errorf("deleteSurface.surfaceId is required") + } + case msg.CallFunction != nil: + if msg.FunctionCallID == "" { + return fmt.Errorf("functionCallId is required") + } + if err := v.validateFunctionCallV010(*msg.CallFunction, 0, true); err != nil { + return fmt.Errorf("callFunction: %w", err) + } + case msg.ActionResponse != nil: + if msg.ActionID == "" { + return fmt.Errorf("actionId is required") + } + if err := validateActionResponseV010(*msg.ActionResponse); err != nil { + return fmt.Errorf("actionResponse: %w", err) + } + } + return nil +} + +func (v *Validator) validateComponentsV010(components []a2uiv010.Component, known map[string]bool) error { + ids := make(map[string]int, len(components)) + for i, component := range components { + if err := v.validateComponentV010(component); err != nil { + return fmt.Errorf("component[%d] (%s): %w", i, component.ID, err) + } + if _, ok := ids[component.ID]; ok { + return validationError(ValidationDuplicateComponent, "", component.ID, "", "", fmt.Sprintf("duplicate component id %q", component.ID)) + } + ids[component.ID] = i + } + graph := make(map[string][]string, len(components)) + for _, component := range components { + refs, err := componentRefsV010(component) + if err != nil { + return fmt.Errorf("component %q: %w", component.ID, err) + } + graph[component.ID] = nil + for _, ref := range refs { + if _, ok := ids[ref]; ok { + graph[component.ID] = append(graph[component.ID], ref) + continue + } + if known != nil && known[ref] { + continue + } + return validationError(ValidationUnknownComponentRef, "", component.ID, ref, "", fmt.Sprintf("component %q references unknown component %q", component.ID, ref)) + } + } + if _, ok := ids["root"]; !ok && len(known) == 0 { + return validationError(ValidationMissingRootComponent, "", "root", "", "", fmt.Sprintf("components must include id %q", "root")) + } + if _, ok := ids["root"]; ok { + if err := validateTopology(graph, "root"); err != nil { + return err + } + } + return nil +} + +func (v *Validator) validateComponentV010(component a2uiv010.Component) error { + if component.ID == "" { + return fmt.Errorf("id is required") + } + componentType := component.ComponentType() + if componentType == "" { + return fmt.Errorf("exactly one concrete component type must be set") + } + if len(v.allowedComponents) > 0 { + if _, ok := v.allowedComponents[componentType]; !ok { + return validationError(ValidationUnknownComponentType, "", component.ID, "", "", fmt.Sprintf("component type %q is not allowed by the selected catalog", componentType)) + } + } + for _, check := range component.Checks { + if check.Message == "" { + return fmt.Errorf("check message is required") + } + if err := v.validateDynamicBooleanV010(check.Condition, 0); err != nil { + return fmt.Errorf("check condition: %w", err) + } + } + if component.Accessibility != nil { + if component.Accessibility.Label != nil { + if err := v.validateDynamicStringV010(*component.Accessibility.Label, 0); err != nil { + return fmt.Errorf("accessibility.label: %w", err) + } + } + if component.Accessibility.Description != nil { + if err := v.validateDynamicStringV010(*component.Accessibility.Description, 0); err != nil { + return fmt.Errorf("accessibility.description: %w", err) + } + } + } + switch { + case component.Text != nil: + return v.validateTextComponentV010(*component.Text) + case component.Image != nil: + return v.validateImageComponentV010(*component.Image) + case component.Icon != nil: + return v.validateIconComponentV010(*component.Icon) + case component.Video != nil: + return v.validateVideoComponentV010(*component.Video) + case component.AudioPlayer != nil: + return v.validateAudioPlayerComponentV010(*component.AudioPlayer) + case component.Row != nil: + return v.validateContainerChildrenV010(component.Row.Children) + case component.Column != nil: + return v.validateContainerChildrenV010(component.Column.Children) + case component.List != nil: + return v.validateContainerChildrenV010(component.List.Children) + case component.Card != nil: + if component.Card.Child == "" { + return fmt.Errorf("card.child is required") + } + case component.Tabs != nil: + if len(component.Tabs.Tabs) == 0 { + return fmt.Errorf("tabs.tabs must not be empty") + } + for _, tab := range component.Tabs.Tabs { + if tab.Child == "" { + return fmt.Errorf("tabs.child is required") + } + if err := v.validateDynamicStringV010(tab.Title, 0); err != nil { + return fmt.Errorf("tabs.title: %w", err) + } + } + case component.Modal != nil: + if component.Modal.Content == "" || component.Modal.Trigger == "" { + return fmt.Errorf("modal.content and modal.trigger are required") + } + case component.Divider != nil: + return nil + case component.Button != nil: + if component.Button.Child == "" { + return fmt.Errorf("button.child is required") + } + if err := v.validateActionV010(component.Button.Action, 0); err != nil { + return fmt.Errorf("button.action: %w", err) + } + case component.TextField != nil: + if err := v.validateDynamicStringV010(component.TextField.Label, 0); err != nil { + return fmt.Errorf("textField.label: %w", err) + } + if component.TextField.Value != nil { + if err := v.validateDynamicStringV010(*component.TextField.Value, 0); err != nil { + return fmt.Errorf("textField.value: %w", err) + } + } + if component.TextField.Placeholder != nil { + if err := v.validateDynamicStringV010(*component.TextField.Placeholder, 0); err != nil { + return fmt.Errorf("textField.placeholder: %w", err) + } + } + case component.CheckBox != nil: + if err := v.validateDynamicStringV010(component.CheckBox.Label, 0); err != nil { + return fmt.Errorf("checkBox.label: %w", err) + } + if err := v.validateDynamicBooleanV010(component.CheckBox.Value, 0); err != nil { + return fmt.Errorf("checkBox.value: %w", err) + } + case component.ChoicePicker != nil: + if len(component.ChoicePicker.Options) == 0 { + return fmt.Errorf("choicePicker.options must not be empty") + } + if component.ChoicePicker.Label != nil { + if err := v.validateDynamicStringV010(*component.ChoicePicker.Label, 0); err != nil { + return fmt.Errorf("choicePicker.label: %w", err) + } + } + for _, option := range component.ChoicePicker.Options { + if option.Value == "" { + return fmt.Errorf("choicePicker option value is required") + } + if err := v.validateDynamicStringV010(option.Label, 0); err != nil { + return fmt.Errorf("choicePicker option label: %w", err) + } + } + if err := v.validateDynamicStringListV010(component.ChoicePicker.Value, 0); err != nil { + return fmt.Errorf("choicePicker.value: %w", err) + } + case component.Slider != nil: + if err := v.validateDynamicNumberV010(component.Slider.Value, 0); err != nil { + return fmt.Errorf("slider.value: %w", err) + } + if component.Slider.Label != nil { + if err := v.validateDynamicStringV010(*component.Slider.Label, 0); err != nil { + return fmt.Errorf("slider.label: %w", err) + } + } + case component.DateTimeInput != nil: + if err := v.validateDynamicStringV010(component.DateTimeInput.Value, 0); err != nil { + return fmt.Errorf("dateTimeInput.value: %w", err) + } + for name, value := range map[string]*a2uiv010.DynamicString{ + "label": component.DateTimeInput.Label, + "max": component.DateTimeInput.Max, + "min": component.DateTimeInput.Min, + } { + if value != nil { + if err := v.validateDynamicStringV010(*value, 0); err != nil { + return fmt.Errorf("dateTimeInput.%s: %w", name, err) + } + } + } + } + return nil +} + +func (v *Validator) validateTextComponentV010(component a2uiv010.TextComponent) error { + return v.validateDynamicStringV010(component.Text, 0) +} + +func (v *Validator) validateImageComponentV010(component a2uiv010.ImageComponent) error { + if err := v.validateDynamicStringV010(component.URL, 0); err != nil { + return err + } + if component.Description != nil { + if err := v.validateDynamicStringV010(*component.Description, 0); err != nil { + return err + } + } + return nil +} + +func (v *Validator) validateIconComponentV010(component a2uiv010.IconComponent) error { + if component.Name.Name == nil && component.Name.Path == nil { + return fmt.Errorf("icon.name is required") + } + return nil +} + +func (v *Validator) validateVideoComponentV010(component a2uiv010.VideoComponent) error { + if err := v.validateDynamicStringV010(component.URL, 0); err != nil { + return err + } + if component.PosterURL != nil { + return v.validateDynamicStringV010(*component.PosterURL, 0) + } + return nil +} + +func (v *Validator) validateAudioPlayerComponentV010(component a2uiv010.AudioPlayerComponent) error { + if err := v.validateDynamicStringV010(component.URL, 0); err != nil { + return err + } + if component.Description != nil { + return v.validateDynamicStringV010(*component.Description, 0) + } + return nil +} + +func (v *Validator) validateContainerChildrenV010(children a2uiv010.ChildList) error { + if len(children.IDs) == 0 && children.Template == nil { + return fmt.Errorf("children must not be empty") + } + if children.Template != nil { + if children.Template.ComponentID == "" { + return fmt.Errorf("children.template.componentId is required") + } + if err := validatePath(children.Template.Path, false); err != nil { + return fmt.Errorf("children.template.path: %w", err) + } + } + return nil +} + +func (v *Validator) validateActionV010(action a2uiv010.Action, depth int) error { + switch { + case action.Event != nil && action.FunctionCall != nil: + return fmt.Errorf("action must not have both event and functionCall") + case action.Event != nil: + if action.Event.Name == "" { + return fmt.Errorf("event.name is required") + } + if action.Event.ResponsePath != "" { + if err := validatePath(action.Event.ResponsePath, false); err != nil { + return fmt.Errorf("event.responsePath: %w", err) + } + } + for key, value := range action.Event.Context { + if err := v.validateDynamicValueV010(value, depth+1); err != nil { + return fmt.Errorf("event.context[%q]: %w", key, err) + } + } + case action.FunctionCall != nil: + if err := v.validateFunctionCallV010(*action.FunctionCall, depth+1, false); err != nil { + return err + } + default: + return fmt.Errorf("action must have event or functionCall") + } + return nil +} + +func (v *Validator) validateDynamicStringV010(value a2uiv010.DynamicString, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCallV010(*value.FunctionCall, depth+1, false) + default: + return fmt.Errorf("dynamic string has no value") + } +} + +func (v *Validator) validateDynamicNumberV010(value a2uiv010.DynamicNumber, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCallV010(*value.FunctionCall, depth+1, false) + default: + return fmt.Errorf("dynamic number has no value") + } +} + +func (v *Validator) validateDynamicBooleanV010(value a2uiv010.DynamicBoolean, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCallV010(*value.FunctionCall, depth+1, false) + default: + return fmt.Errorf("dynamic boolean has no value") + } +} + +func (v *Validator) validateDynamicStringListV010(value a2uiv010.DynamicStringList, depth int) error { + switch { + case value.Literal != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCallV010(*value.FunctionCall, depth+1, false) + default: + return fmt.Errorf("dynamic string list has no value") + } +} + +func (v *Validator) validateDynamicValueV010(value a2uiv010.DynamicValue, depth int) error { + switch { + case value.String != nil, value.Number != nil, value.Bool != nil, value.Array != nil: + return nil + case value.Binding != nil: + return validatePath(value.Binding.Path, false) + case value.FunctionCall != nil: + return v.validateFunctionCallV010(*value.FunctionCall, depth+1, false) + default: + return fmt.Errorf("dynamic value has no value") + } +} + +func (v *Validator) validateFunctionCallV010(call a2uiv010.FunctionCall, depth int, requireRemote bool) error { + if depth > 32 { + return fmt.Errorf("function call recursion depth exceeded") + } + if call.Call == "" { + return fmt.Errorf("function call name is required") + } + if len(v.allowedFunctions) > 0 { + if _, ok := v.allowedFunctions[call.Call]; !ok { + return validationError(ValidationUnknownFunction, "", "", "", call.Call, fmt.Sprintf("unknown function %q", call.Call)) + } + } + if requireRemote { + if call.ReturnType == "" { + return fmt.Errorf("returnType is required") + } + } + for key, arg := range call.Args { + if err := v.validateFunctionArgV010(arg, depth+1); err != nil { + return fmt.Errorf("function arg %q: %w", key, err) + } + } + return nil +} + +func (v *Validator) validateFunctionArgV010(arg any, depth int) error { + switch value := arg.(type) { + case nil, string, bool, float64, int: + return nil + case []string: + return nil + case []any: + for i, item := range value { + if err := v.validateFunctionArgV010(item, depth+1); err != nil { + return fmt.Errorf("[%d]: %w", i, err) + } + } + return nil + case map[string]any: + if _, ok := value["path"]; ok { + path, _ := value["path"].(string) + return validatePath(path, false) + } + if _, ok := value["call"]; ok { + data, err := json.Marshal(value) + if err != nil { + return err + } + var call a2uiv010.FunctionCall + if err := json.Unmarshal(data, &call); err != nil { + return err + } + return v.validateFunctionCallV010(call, depth+1, false) + } + keys := make([]string, 0, len(value)) + for key := range value { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + if err := v.validateFunctionArgV010(value[key], depth+1); err != nil { + return fmt.Errorf("%s: %w", key, err) + } + } + return nil + case a2uiv010.DynamicValue: + return v.validateDynamicValueV010(value, depth+1) + case a2uiv010.DynamicString: + return v.validateDynamicStringV010(value, depth+1) + case a2uiv010.DynamicNumber: + return v.validateDynamicNumberV010(value, depth+1) + case a2uiv010.DynamicBoolean: + return v.validateDynamicBooleanV010(value, depth+1) + case a2uiv010.DynamicStringList: + return v.validateDynamicStringListV010(value, depth+1) + default: + return nil + } +} + +func validateActionResponseV010(response a2uiv010.ActionResponse) error { + hasValue := response.HasValue || response.Value != nil + hasError := response.Error != nil + switch { + case hasValue && hasError: + return fmt.Errorf("must not have both value and error") + case hasValue: + return nil + case hasError: + if response.Error.Code == "" { + return fmt.Errorf("error.code is required") + } + if response.Error.Message == "" { + return fmt.Errorf("error.message is required") + } + return nil + default: + return fmt.Errorf("must have value or error") + } +} + +func componentRefsV010(component a2uiv010.Component) ([]string, error) { + switch { + case component.Button != nil: + return []string{component.Button.Child}, nil + case component.Card != nil: + return []string{component.Card.Child}, nil + case component.Column != nil: + return childListRefsV010(component.Column.Children) + case component.List != nil: + return childListRefsV010(component.List.Children) + case component.Row != nil: + return childListRefsV010(component.Row.Children) + case component.Modal != nil: + return []string{component.Modal.Trigger, component.Modal.Content}, nil + case component.Tabs != nil: + refs := make([]string, 0, len(component.Tabs.Tabs)) + for _, tab := range component.Tabs.Tabs { + refs = append(refs, tab.Child) + } + return refs, nil + default: + return nil, nil + } +} + +func childListRefsV010(children a2uiv010.ChildList) ([]string, error) { + if children.Template != nil { + return []string{children.Template.ComponentID}, nil + } + return append([]string(nil), children.IDs...), nil +} + +func countSetV010(values ...bool) int { + var count int + for _, value := range values { + if value { + count++ + } + } + return count +} diff --git a/agent_sdks/go/a2uistream/doc.go b/agent_sdks/go/a2uistream/doc.go new file mode 100644 index 0000000000..70dab45ed6 --- /dev/null +++ b/agent_sdks/go/a2uistream/doc.go @@ -0,0 +1,3 @@ +// Package a2uistream provides a streaming parser for A2UI messages +// embedded in LLM responses. +package a2uistream diff --git a/agent_sdks/go/a2uistream/example_test.go b/agent_sdks/go/a2uistream/example_test.go new file mode 100644 index 0000000000..3595ec16bc --- /dev/null +++ b/agent_sdks/go/a2uistream/example_test.go @@ -0,0 +1,82 @@ +package a2uistream_test + +import ( + "fmt" + "io" + "strings" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2uistream" +) + +func ExampleReader_Next() { + input := `Before {"version":"v0.9","deleteSurface":{"surfaceId":"old"}} after` + r := a2uistream.NewReader(strings.NewReader(input)) + for { + part, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + if part.Text != "" { + fmt.Println(strings.TrimSpace(part.Text)) + } + for _, msg := range part.Messages { + fmt.Println(msg.DeleteSurface.SurfaceID) + } + } + // Output: + // Before + // old + // after +} + +func ExampleReader_Next_payload() { + input := `Before {"version":"v0.10","functionCallId":"call-1","callFunction":{"call":"lookup","returnType":"string"}}` + r := a2uistream.NewReader(strings.NewReader(input)) + for { + part, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + for _, payload := range part.Payload { + fmt.Println(payload["version"]) + fmt.Println(payload["functionCallId"]) + } + } + // Output: + // v0.10 + // call-1 +} + +func ExampleFixPayload() { + payload, err := a2uistream.FixPayload(`{"type": “Text”, "text": "Hello",}`) + if err != nil { + panic(err) + } + fmt.Println(payload[0]["type"]) + fmt.Println(payload[0]["text"]) + // Output: + // Text + // Hello +} + +func ExampleParseResponse() { + parts, err := a2uistream.ParseResponse(`Intro +[{"id":"card"}] +Done`) + if err != nil { + panic(err) + } + fmt.Println(parts[0].Text) + fmt.Println(parts[0].Payload[0]["id"]) + fmt.Println(parts[1].Text) + // Output: + // Intro + // card + // Done +} diff --git a/agent_sdks/go/a2uistream/parser.go b/agent_sdks/go/a2uistream/parser.go new file mode 100644 index 0000000000..a391b33d98 --- /dev/null +++ b/agent_sdks/go/a2uistream/parser.go @@ -0,0 +1,560 @@ +package a2uistream + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "strings" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" +) + +const ( + openTag = "" + closeTag = "" +) + +// ResponsePart is a segment of the LLM response. +// A part contains either conversational text, parsed A2UI messages, or both +// (text preceding a JSON block and the messages extracted from it). +type ResponsePart struct { + Text string // conversational text + Messages []a2ui.ServerMessage // A2UI v0.9 messages (nil if text-only) + Payload []map[string]any // version-neutral A2UI messages (nil if text-only) +} + +// Parser incrementally parses A2UI messages from text chunks. +// +// The parser handles text interleaved with A2UI JSON, where JSON blocks +// may be wrapped in tags or appear as bare JSON objects +// containing recognized A2UI message keys. +type Parser struct { + buf strings.Builder + inTag bool + jsonBuf strings.Builder + braceDepth int + inString bool + escaped bool + jsonStart int // index in jsonBuf where current top-level object starts +} + +// Reader parses A2UI response parts from an io.Reader. +type Reader struct { + r io.Reader + parser *Parser + buf []byte + queue []ResponsePart + done bool +} + +// NewParser creates a new streaming parser. +func NewParser() *Parser { + return &Parser{} +} + +// NewReader creates a parser that reads from r. +func NewReader(r io.Reader) *Reader { + return &Reader{ + r: r, + parser: NewParser(), + buf: make([]byte, 32*1024), + } +} + +// Next returns the next parsed response part. +func (r *Reader) Next() (ResponsePart, error) { + for len(r.queue) == 0 { + if r.done { + return ResponsePart{}, io.EOF + } + n, err := r.r.Read(r.buf) + if n > 0 { + parts, parseErr := r.parser.ProcessChunk(string(r.buf[:n])) + if parseErr != nil { + return ResponsePart{}, parseErr + } + r.queue = append(r.queue, parts...) + } + if err != nil { + if !errors.Is(err, io.EOF) { + return ResponsePart{}, err + } + r.done = true + parts, parseErr := r.parser.Flush() + if parseErr != nil { + return ResponsePart{}, parseErr + } + r.queue = append(r.queue, parts...) + } + } + part := r.queue[0] + copy(r.queue, r.queue[1:]) + r.queue = r.queue[:len(r.queue)-1] + return part, nil +} + +// ProcessChunk feeds a chunk of text and returns any complete parts found. +func (p *Parser) ProcessChunk(chunk string) ([]ResponsePart, error) { + p.buf.WriteString(chunk) + return p.drain() +} + +// Flush returns any remaining buffered content as a text part. +func (p *Parser) Flush() ([]ResponsePart, error) { + // If we're inside a tag, try to parse whatever JSON we have. + if p.inTag && p.jsonBuf.Len() > 0 { + parts, err := p.finishJSON() + if err != nil { + return nil, err + } + p.inTag = false + p.resetJSON() + // Append any remaining buffer text. + if p.buf.Len() > 0 { + text := p.buf.String() + p.buf.Reset() + if len(parts) > 0 && parts[0].Messages == nil { + parts[0].Text += text + } else { + parts = append(parts, ResponsePart{Text: text}) + } + } + return parts, nil + } + if p.buf.Len() == 0 { + return nil, nil + } + text := p.buf.String() + p.buf.Reset() + return []ResponsePart{{Text: text}}, nil +} + +// Reset clears all parser state for reuse. +func (p *Parser) Reset() { + p.buf.Reset() + p.inTag = false + p.resetJSON() +} + +func (p *Parser) resetJSON() { + p.jsonBuf.Reset() + p.braceDepth = 0 + p.inString = false + p.escaped = false + p.jsonStart = 0 +} + +func (p *Parser) drain() ([]ResponsePart, error) { + var parts []ResponsePart + for { + if !p.inTag { + got, done := p.scanForOpen(&parts) + if done || !got { + break + } + } + if p.inTag { + got, err := p.scanForClose(&parts) + if err != nil { + return parts, err + } + if !got { + break + } + } + } + return parts, nil +} + +// scanForOpen looks for in the buffer. +// Returns (found, done). done=true means we should stop draining. +func (p *Parser) scanForOpen(parts *[]ResponsePart) (bool, bool) { + s := p.buf.String() + tagIdx := strings.Index(s, openTag) + limit := len(s) + if tagIdx >= 0 { + limit = tagIdx + } + for searchFrom := 0; searchFrom < limit; { + rel := strings.IndexByte(s[searchFrom:limit], '{') + if rel < 0 { + break + } + idx := searchFrom + rel + if !isBareJSONBoundary(s, idx) { + searchFrom = idx + 1 + continue + } + candidate := s[idx:] + if !possibleBareMessagePrefix(candidate) { + searchFrom = idx + 1 + continue + } + end, complete := scanJSONObject(candidate) + if !complete { + if idx > 0 { + *parts = append(*parts, ResponsePart{Text: s[:idx]}) + p.buf.Reset() + p.buf.WriteString(candidate) + } + return false, true + } + obj := candidate[:end] + msg, hasMessage := parseMessage(obj) + payload, hasPayload := parsePayloadObject(obj) + if hasMessage || hasPayload { + if idx > 0 { + *parts = append(*parts, ResponsePart{Text: s[:idx]}) + } + part := ResponsePart{} + if hasMessage { + part.Messages = []a2ui.ServerMessage{msg} + } + if hasPayload { + part.Payload = []map[string]any{payload} + } + *parts = append(*parts, part) + p.buf.Reset() + p.buf.WriteString(candidate[end:]) + return true, false + } + searchFrom = idx + 1 + } + if tagIdx >= 0 { + if tagIdx > 0 { + *parts = append(*parts, ResponsePart{Text: s[:tagIdx]}) + } + p.buf.Reset() + p.buf.WriteString(s[tagIdx+len(openTag):]) + p.inTag = true + p.resetJSON() + return true, false + } + // Keep potential partial tag prefix in the buffer. + keepLen := partialSuffix(s, openTag) + if safeLen := len(s) - keepLen; safeLen > 0 { + *parts = append(*parts, ResponsePart{Text: s[:safeLen]}) + p.buf.Reset() + p.buf.WriteString(s[safeLen:]) + } + return false, true +} + +// scanForClose looks for in the buffer while processing JSON. +func (p *Parser) scanForClose(parts *[]ResponsePart) (bool, error) { + s := p.buf.String() + idx := strings.Index(s, closeTag) + if idx >= 0 { + // Process everything before the close tag as JSON. + p.feedJSON(s[:idx]) + jsonParts, err := p.finishJSON() + if err != nil { + return false, err + } + *parts = append(*parts, jsonParts...) + p.inTag = false + p.resetJSON() + p.buf.Reset() + p.buf.WriteString(s[idx+len(closeTag):]) + return true, nil + } + // No close tag found yet. Process what we can, keeping a safe suffix. + keepLen := partialSuffix(s, closeTag) + if safeLen := len(s) - keepLen; safeLen > 0 { + p.feedJSON(s[:safeLen]) + p.buf.Reset() + p.buf.WriteString(s[safeLen:]) + } + return false, nil +} + +// feedJSON processes characters of JSON content, tracking brace depth and +// extracting complete top-level objects. +func (p *Parser) feedJSON(s string) { + for _, c := range s { + if p.inString { + p.jsonBuf.WriteRune(c) + if p.escaped { + p.escaped = false + continue + } + switch c { + case '\\': + p.escaped = true + case '"': + p.inString = false + } + continue + } + + switch c { + case '"': + p.inString = true + p.jsonBuf.WriteRune(c) + case '{': + if p.braceDepth == 0 { + p.jsonStart = p.jsonBuf.Len() + } + p.braceDepth++ + p.jsonBuf.WriteRune(c) + case '}': + p.braceDepth-- + p.jsonBuf.WriteRune(c) + // Object complete — handled in finishJSON or next drain. + default: + if p.braceDepth > 0 { + p.jsonBuf.WriteRune(c) + } + } + } +} + +// finishJSON extracts all complete JSON objects from the json buffer. +func (p *Parser) finishJSON() ([]ResponsePart, error) { + return p.extractObjects() +} + +// extractObjects scans the jsonBuf for complete top-level JSON objects +// and parses them as ServerMessages. +func (p *Parser) extractObjects() ([]ResponsePart, error) { + raw := p.jsonBuf.String() + var msgs []a2ui.ServerMessage + var payload []map[string]any + + depth := 0 + inStr := false + esc := false + start := -1 + + for i, c := range raw { + if inStr { + if esc { + esc = false + continue + } + switch c { + case '\\': + esc = true + case '"': + inStr = false + } + continue + } + switch c { + case '"': + inStr = true + case '{': + if depth == 0 { + start = i + } + depth++ + case '}': + depth-- + if depth == 0 && start >= 0 { + obj := raw[start : i+1] + msg, hasMessage := parseMessage(obj) + if hasMessage { + msgs = append(msgs, msg) + } + if p, ok := parsePayloadObject(obj); ok { + payload = append(payload, p) + } + start = -1 + } + } + } + + if len(msgs) == 0 && len(payload) == 0 { + return nil, nil + } + return []ResponsePart{{Messages: msgs, Payload: payload}}, nil +} + +// isA2UIMessage returns true if the message has at least one recognized payload. +func isA2UIMessage(m a2ui.ServerMessage) bool { + return m.CreateSurface != nil || + m.UpdateComponents != nil || + m.UpdateDataModel != nil || + m.DeleteSurface != nil +} + +func parseMessage(obj string) (a2ui.ServerMessage, bool) { + var msg a2ui.ServerMessage + if err := json.Unmarshal([]byte(obj), &msg); err != nil || !isA2UIMessage(msg) { + return a2ui.ServerMessage{}, false + } + return msg, true +} + +func parsePayloadObject(obj string) (map[string]any, bool) { + dec := json.NewDecoder(bytes.NewReader([]byte(obj))) + dec.UseNumber() + var payload map[string]any + if err := dec.Decode(&payload); err != nil || !isA2UIPayload(payload) { + return nil, false + } + return payload, true +} + +func isA2UIPayload(payload map[string]any) bool { + for _, key := range payloadMessageKeys { + if _, ok := payload[key]; ok { + return true + } + } + return false +} + +// partialSuffix returns the length of the longest suffix of s that +// is a prefix of tag. This prevents splitting a tag across chunks. +func partialSuffix(s, tag string) int { + maxCheck := len(tag) - 1 + if maxCheck > len(s) { + maxCheck = len(s) + } + for i := maxCheck; i > 0; i-- { + if strings.HasSuffix(s, tag[:i]) { + return i + } + } + return 0 +} + +var bareMessageKeys = []string{ + "version", + "functionCallId", + "actionId", + "wantResponse", + "createSurface", + "updateComponents", + "updateDataModel", + "deleteSurface", + "callFunction", + "actionResponse", +} + +var payloadMessageKeys = []string{ + "createSurface", + "updateComponents", + "updateDataModel", + "deleteSurface", + "callFunction", + "actionResponse", +} + +func isBareJSONBoundary(s string, idx int) bool { + if idx == 0 { + return true + } + switch s[idx-1] { + case ' ', '\t', '\n', '\r': + return true + default: + return false + } +} + +func possibleBareMessagePrefix(s string) bool { + if s == "" || s[0] != '{' { + return false + } + i := 1 + for i < len(s) && isJSONSpace(s[i]) { + i++ + } + if i == len(s) { + return true + } + if s[i] != '"' { + return false + } + i++ + start := i + for i < len(s) && isJSONKeyChar(s[i]) { + i++ + } + fragment := s[start:i] + if i == len(s) || s[i] != '"' { + return hasKnownKeyPrefix(fragment) + } + if !isKnownKey(fragment) { + return false + } + i++ + for i < len(s) && isJSONSpace(s[i]) { + i++ + } + return i == len(s) || s[i] == ':' +} + +func scanJSONObject(s string) (int, bool) { + if s == "" || s[0] != '{' { + return 0, false + } + depth := 0 + inString := false + escaped := false + for i := 0; i < len(s); i++ { + c := s[i] + if inString { + if escaped { + escaped = false + continue + } + switch c { + case '\\': + escaped = true + case '"': + inString = false + } + continue + } + switch c { + case '"': + inString = true + case '{': + depth++ + case '}': + depth-- + if depth == 0 { + return i + 1, true + } + } + } + return 0, false +} + +func hasKnownKeyPrefix(fragment string) bool { + if fragment == "" { + return true + } + for _, key := range bareMessageKeys { + if strings.HasPrefix(key, fragment) { + return true + } + } + return false +} + +func isKnownKey(fragment string) bool { + for _, key := range bareMessageKeys { + if key == fragment { + return true + } + } + return false +} + +func isJSONKeyChar(b byte) bool { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') +} + +func isJSONSpace(b byte) bool { + switch b { + case ' ', '\t', '\n', '\r': + return true + default: + return false + } +} diff --git a/agent_sdks/go/a2uistream/parser_test.go b/agent_sdks/go/a2uistream/parser_test.go new file mode 100644 index 0000000000..6fb66f8050 --- /dev/null +++ b/agent_sdks/go/a2uistream/parser_test.go @@ -0,0 +1,517 @@ +package a2uistream + +import ( + "encoding/json" + "io" + "strings" + "testing" + + "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" +) + +func TestPureText(t *testing.T) { + p := NewParser() + parts, err := p.ProcessChunk("Hello, world!") + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + text := collectText(parts) + if text != "Hello, world!" { + t.Errorf("got %q, want %q", text, "Hello, world!") + } + if msgs := collectMessages(parts); len(msgs) != 0 { + t.Errorf("expected no messages, got %d", len(msgs)) + } +} + +func TestV010CallFunctionPayload(t *testing.T) { + input := `{"version":"v0.10","functionCallId":"call-1","wantResponse":true,"callFunction":{"call":"lookup","returnType":"string"}}` + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + + if msgs := collectMessages(parts); len(msgs) != 0 { + t.Fatalf("legacy messages = %d, want 0", len(msgs)) + } + payload := collectPayload(parts) + if len(payload) != 1 { + t.Fatalf("payload count = %d, want 1", len(payload)) + } + if got := payload[0]["functionCallId"]; got != "call-1" { + t.Fatalf("functionCallId = %#v, want call-1", got) + } + if got := payload[0]["wantResponse"]; got != true { + t.Fatalf("wantResponse = %#v, want true", got) + } + call, ok := payload[0]["callFunction"].(map[string]any) + if !ok { + t.Fatalf("callFunction = %#v, want object", payload[0]["callFunction"]) + } + if got := call["call"]; got != "lookup" { + t.Fatalf("callFunction.call = %#v, want lookup", got) + } +} + +func TestBareV010CallFunctionPayload(t *testing.T) { + input := `before {"version":"v0.10","functionCallId":"call-1","callFunction":{"call":"lookup","returnType":"string"}} after` + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + + if got := collectText(parts); got != "before after" { + t.Fatalf("text = %q, want %q", got, "before after") + } + payload := collectPayload(parts) + if len(payload) != 1 { + t.Fatalf("payload count = %d, want 1", len(payload)) + } + if got := payload[0]["functionCallId"]; got != "call-1" { + t.Fatalf("functionCallId = %#v, want call-1", got) + } +} + +func TestVersionOnlyJSONRemainsText(t *testing.T) { + p := NewParser() + parts, err := p.ProcessChunk(`prefix {"version":"v0.10"} suffix`) + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + + if text := collectText(parts); text != `prefix {"version":"v0.10"} suffix` { + t.Fatalf("text = %q", text) + } + if payload := collectPayload(parts); len(payload) != 0 { + t.Fatalf("expected no payload, got %d", len(payload)) + } +} + +func TestReaderNext(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "reader", + CatalogID: "cat", + }, + } + data, _ := json.Marshal(msg) + reader := NewReader(strings.NewReader("Before " + string(data) + " after")) + + var parts []ResponsePart + for { + part, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + parts = append(parts, part) + } + if got := collectText(parts); got != "Before after" { + t.Fatalf("text = %q, want %q", got, "Before after") + } + msgs := collectMessages(parts) + if len(msgs) != 1 || msgs[0].CreateSurface == nil { + t.Fatalf("unexpected messages: %+v", msgs) + } +} + +func TestSingleJSONMessage(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "s1", + CatalogID: "cat1", + }, + } + data, _ := json.Marshal(msg) + input := "" + string(data) + "" + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].CreateSurface == nil { + t.Fatal("expected CreateSurface message") + } + if msgs[0].CreateSurface.SurfaceID != "s1" { + t.Errorf("surfaceId = %q, want %q", msgs[0].CreateSurface.SurfaceID, "s1") + } +} + +func TestWrappedInTags(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + UpdateDataModel: &a2ui.UpdateDataModel{ + SurfaceID: "s1", + Value: map[string]any{"name": "Alice"}, + }, + } + data, _ := json.Marshal(msg) + input := "Here is the UI: " + string(data) + " Done." + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + text := collectText(parts) + if text != "Here is the UI: Done." { + t.Errorf("text = %q, want %q", text, "Here is the UI: Done.") + } + msgs := collectMessages(parts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].UpdateDataModel == nil { + t.Fatal("expected UpdateDataModel message") + } +} + +func TestMixedTextAndJSON(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + DeleteSurface: &a2ui.DeleteSurface{ + SurfaceID: "s1", + }, + } + data, _ := json.Marshal(msg) + input := "Removing surface now.\n" + string(data) + "\nAll done." + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].DeleteSurface == nil { + t.Fatal("expected DeleteSurface message") + } +} + +func TestChunkedInput(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "chunked", + CatalogID: "cat", + }, + } + data, _ := json.Marshal(msg) + full := "" + string(data) + "" + + // Split the input into small chunks. + p := NewParser() + var allParts []ResponsePart + for i := 0; i < len(full); i += 7 { + end := i + 7 + if end > len(full) { + end = len(full) + } + parts, err := p.ProcessChunk(full[i:end]) + if err != nil { + t.Fatal(err) + } + allParts = append(allParts, parts...) + } + flush, _ := p.Flush() + allParts = append(allParts, flush...) + + msgs := collectMessages(allParts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].CreateSurface == nil || msgs[0].CreateSurface.SurfaceID != "chunked" { + t.Errorf("unexpected message: %+v", msgs[0]) + } +} + +func TestBareJSONMessage(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + DeleteSurface: &a2ui.DeleteSurface{ + SurfaceID: "s1", + }, + } + data, _ := json.Marshal(msg) + + p := NewParser() + parts, err := p.ProcessChunk(string(data)) + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].DeleteSurface == nil || msgs[0].DeleteSurface.SurfaceID != "s1" { + t.Fatalf("unexpected message: %+v", msgs[0]) + } +} + +func TestChunkedBareJSONMessage(t *testing.T) { + msg := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "bare", + CatalogID: "cat", + }, + } + data, _ := json.Marshal(msg) + + p := NewParser() + var allParts []ResponsePart + for i := 0; i < len(data); i += 5 { + end := i + 5 + if end > len(data) { + end = len(data) + } + parts, err := p.ProcessChunk(string(data[i:end])) + if err != nil { + t.Fatal(err) + } + allParts = append(allParts, parts...) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + allParts = append(allParts, flush...) + + msgs := collectMessages(allParts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].CreateSurface == nil || msgs[0].CreateSurface.SurfaceID != "bare" { + t.Fatalf("unexpected message: %+v", msgs[0]) + } +} + +func TestNonMessageJSONRemainsText(t *testing.T) { + p := NewParser() + parts, err := p.ProcessChunk(`prefix {"hello":"world"} suffix`) + if err != nil { + t.Fatal(err) + } + flush, err := p.Flush() + if err != nil { + t.Fatal(err) + } + parts = append(parts, flush...) + + if text := collectText(parts); text != `prefix {"hello":"world"} suffix` { + t.Fatalf("text = %q", text) + } + if msgs := collectMessages(parts); len(msgs) != 0 { + t.Fatalf("expected no messages, got %d", len(msgs)) + } +} + +func TestMultipleMessagesInOneBlock(t *testing.T) { + msg1 := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "s1", + CatalogID: "cat", + }, + } + msg2 := a2ui.ServerMessage{ + Version: "v0.9", + UpdateComponents: &a2ui.UpdateComponents{ + SurfaceID: "s1", + Components: []a2ui.Component{ + { + ID: "root", + Text: &a2ui.TextComponent{Text: a2ui.StringLiteral("hi")}, + }, + }, + }, + } + d1, _ := json.Marshal(msg1) + d2, _ := json.Marshal(msg2) + input := "" + string(d1) + string(d2) + "" + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 2 { + t.Fatalf("expected 2 messages, got %d", len(msgs)) + } + if msgs[0].CreateSurface == nil { + t.Error("first message should be CreateSurface") + } + if msgs[1].UpdateComponents == nil { + t.Error("second message should be UpdateComponents") + } +} + +func TestMultipleBlocks(t *testing.T) { + msg1 := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "s1", + CatalogID: "cat", + }, + } + msg2 := a2ui.ServerMessage{ + Version: "v0.9", + DeleteSurface: &a2ui.DeleteSurface{ + SurfaceID: "s1", + }, + } + d1, _ := json.Marshal(msg1) + d2, _ := json.Marshal(msg2) + input := "First: " + string(d1) + " Middle " + string(d2) + " End" + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 2 { + t.Fatalf("expected 2 messages, got %d", len(msgs)) + } +} + +func TestEscapedBracesInStrings(t *testing.T) { + // A message where a string value contains braces — the parser must not + // be confused by them. + msg := a2ui.ServerMessage{ + Version: "v0.9", + UpdateDataModel: &a2ui.UpdateDataModel{ + SurfaceID: "s1", + Value: map[string]any{"code": "if (x) { y }"}, + }, + } + data, _ := json.Marshal(msg) + input := "" + string(data) + "" + + p := NewParser() + parts, err := p.ProcessChunk(input) + if err != nil { + t.Fatal(err) + } + flush, _ := p.Flush() + parts = append(parts, flush...) + + msgs := collectMessages(parts) + if len(msgs) != 1 { + t.Fatalf("expected 1 message, got %d", len(msgs)) + } + if msgs[0].UpdateDataModel == nil { + t.Fatal("expected UpdateDataModel") + } +} + +func TestResetAndReuse(t *testing.T) { + p := NewParser() + + msg := a2ui.ServerMessage{ + Version: "v0.9", + CreateSurface: &a2ui.CreateSurface{ + SurfaceID: "s1", + CatalogID: "cat", + }, + } + data, _ := json.Marshal(msg) + + input := "" + string(data) + "" + parts, _ := p.ProcessChunk(input) + flush, _ := p.Flush() + parts = append(parts, flush...) + if len(collectMessages(parts)) != 1 { + t.Fatal("expected 1 message before reset") + } + + p.Reset() + + parts, _ = p.ProcessChunk(input) + flush, _ = p.Flush() + parts = append(parts, flush...) + if len(collectMessages(parts)) != 1 { + t.Fatal("expected 1 message after reset") + } +} + +func collectText(parts []ResponsePart) string { + var b string + for _, p := range parts { + b += p.Text + } + return b +} + +func collectMessages(parts []ResponsePart) []a2ui.ServerMessage { + var msgs []a2ui.ServerMessage + for _, p := range parts { + msgs = append(msgs, p.Messages...) + } + return msgs +} + +func collectPayload(parts []ResponsePart) []map[string]any { + var payload []map[string]any + for _, p := range parts { + payload = append(payload, p.Payload...) + } + return payload +} diff --git a/agent_sdks/go/a2uistream/payload.go b/agent_sdks/go/a2uistream/payload.go new file mode 100644 index 0000000000..d97be3b895 --- /dev/null +++ b/agent_sdks/go/a2uistream/payload.go @@ -0,0 +1,148 @@ +package a2uistream + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// PayloadPart is a generic A2UI response segment. +// +// It is useful before binding a payload to a specific protocol version. +type PayloadPart struct { + Text string + Payload []map[string]any +} + +// HasParts reports whether s contains a complete A2UI JSON tag pair. +func HasParts(s string) bool { + open := strings.Index(s, openTag) + if open < 0 { + return false + } + return strings.Contains(s[open+len(openTag):], closeTag) +} + +// ParseResponse parses tagged A2UI JSON blocks from s. +func ParseResponse(s string) ([]PayloadPart, error) { + var parts []PayloadPart + lastEnd := 0 + for { + open := strings.Index(s[lastEnd:], openTag) + if open < 0 { + break + } + open += lastEnd + close := strings.Index(s[open+len(openTag):], closeTag) + if close < 0 { + break + } + close += open + len(openTag) + text := strings.TrimSpace(s[lastEnd:open]) + raw := strings.TrimSpace(s[open+len(openTag) : close]) + if raw == "" { + return nil, fmt.Errorf("a2uistream: A2UI JSON part is empty") + } + payload, err := FixPayload(raw) + if err != nil { + return nil, fmt.Errorf("a2uistream: failed to parse A2UI JSON: %w", err) + } + parts = append(parts, PayloadPart{Text: text, Payload: payload}) + lastEnd = close + len(closeTag) + } + if len(parts) == 0 { + return nil, fmt.Errorf("a2uistream: A2UI tags %q and %q not found in response", openTag, closeTag) + } + if trailing := strings.TrimSpace(s[lastEnd:]); trailing != "" { + parts = append(parts, PayloadPart{Text: trailing}) + } + return parts, nil +} + +// FixPayload parses common LLM-produced JSON payload shapes. +// +// It normalizes smart quotes, removes trailing commas, and wraps a single +// object in a list. The returned slice contains one map per payload object. +func FixPayload(s string) ([]map[string]any, error) { + s = strings.TrimSpace(stripMarkdownFence(normalizeSmartQuotes(s))) + s = removeTrailingCommas(s) + if strings.HasPrefix(s, "{") { + s = "[" + s + "]" + } + var out []map[string]any + dec := json.NewDecoder(strings.NewReader(s)) + dec.UseNumber() + if err := dec.Decode(&out); err != nil { + return nil, fmt.Errorf("a2uistream: parse payload: %w", err) + } + if strings.TrimSpace(s) == "" { + return nil, fmt.Errorf("a2uistream: empty payload") + } + return out, nil +} + +func normalizeSmartQuotes(s string) string { + replacer := strings.NewReplacer( + "\u201c", `"`, + "\u201d", `"`, + "\u2018", `'`, + "\u2019", `'`, + ) + return replacer.Replace(s) +} + +func stripMarkdownFence(s string) string { + s = strings.TrimSpace(s) + if !strings.HasPrefix(s, "```") { + return s + } + lines := strings.Split(s, "\n") + if len(lines) < 2 { + return s + } + if strings.HasPrefix(strings.TrimSpace(lines[len(lines)-1]), "```") { + return strings.Join(lines[1:len(lines)-1], "\n") + } + return s +} + +func removeTrailingCommas(s string) string { + var out bytes.Buffer + inString := false + escaped := false + for i := 0; i < len(s); i++ { + c := s[i] + if inString { + out.WriteByte(c) + if escaped { + escaped = false + continue + } + switch c { + case '\\': + escaped = true + case '"': + inString = false + } + continue + } + switch c { + case '"': + inString = true + out.WriteByte(c) + case ',': + j := i + 1 + for j < len(s) && isJSONSpace(s[j]) { + j++ + } + if j < len(s) && (s[j] == '}' || s[j] == ']') { + continue + } + out.WriteByte(c) + default: + out.WriteByte(c) + } + } + return out.String() +} diff --git a/agent_sdks/go/a2uistream/payload_conformance_test.go b/agent_sdks/go/a2uistream/payload_conformance_test.go new file mode 100644 index 0000000000..950d8c8998 --- /dev/null +++ b/agent_sdks/go/a2uistream/payload_conformance_test.go @@ -0,0 +1,175 @@ +package a2uistream + +import ( + "reflect" + "testing" +) + +func TestPayloadConformance(t *testing.T) { + for _, tc := range payloadConformanceCases() { + t.Run(tc.name, func(t *testing.T) { + switch tc.action { + case "parse_full": + got, err := ParseResponse(tc.input) + if tc.wantErr != "" { + if err == nil { + t.Fatalf("ParseResponse succeeded, want error matching %q", tc.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tc.wantParts) { + t.Fatalf("ParseResponse = %#v, want %#v", got, tc.wantParts) + } + case "has_parts": + if got := HasParts(tc.input); got != tc.wantHasParts { + t.Fatalf("HasParts = %v, want %v", got, tc.wantHasParts) + } + case "fix_payload": + got, err := FixPayload(tc.input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tc.wantPayload) { + t.Fatalf("FixPayload = %#v, want %#v", got, tc.wantPayload) + } + default: + t.Fatalf("unsupported action %q", tc.action) + } + }) + } +} + +type payloadConformanceCase struct { + name string + action string + input string + wantErr string + wantParts []PayloadPart + wantHasParts bool + wantPayload []map[string]any +} + +func payloadConformanceCases() []payloadConformanceCase { + // These cases mirror the has_parts and fix_payload cases in + // agent_sdks/conformance/suites/parser.yaml. + return []payloadConformanceCase{ + { + name: "test_parse_empty_response", + action: "parse_full", + input: "", + wantErr: "not found in response", + }, + { + name: "test_parse_response_only_text_no_tags", + action: "parse_full", + input: "Only text, no tags.", + wantErr: "not found in response", + }, + { + name: "test_parse_response_empty_tags", + action: "parse_full", + input: "", + wantErr: "A2UI JSON part is empty", + }, + { + name: "test_parse_response_only_json_with_tags", + action: "parse_full", + input: `[{"id": "test"}]`, + wantParts: []PayloadPart{ + {Payload: []map[string]any{{"id": "test"}}}, + }, + }, + { + name: "test_parse_response_with_text_and_tags", + action: "parse_full", + input: "Hello\n[{\"id\": \"test\"}]", + wantParts: []PayloadPart{ + {Text: "Hello", Payload: []map[string]any{{"id": "test"}}}, + }, + }, + { + name: "test_parse_response_with_trailing_text", + action: "parse_full", + input: "Hello\n[{\"id\": \"test\"}]\nGoodbye", + wantParts: []PayloadPart{ + {Text: "Hello", Payload: []map[string]any{{"id": "test"}}}, + {Text: "Goodbye"}, + }, + }, + { + name: "test_parse_response_multiple_blocks", + action: "parse_full", + input: "Part 1\n\n[{\"id\": \"1\"}]\n\nPart 2\n\n[{\"id\": \"2\"}]\n\nPart 3", + wantParts: []PayloadPart{ + {Text: "Part 1", Payload: []map[string]any{{"id": "1"}}}, + {Text: "Part 2", Payload: []map[string]any{{"id": "2"}}}, + {Text: "Part 3"}, + }, + }, + { + name: "test_parse_response_with_markdown_blocks", + action: "parse_full", + input: "Text\n\n```json\n[{\"id\": \"test\"}]\n```\n", + wantParts: []PayloadPart{ + {Text: "Text", Payload: []map[string]any{{"id": "test"}}}, + }, + }, + { + name: "test_parse_response_invalid_json", + action: "parse_full", + input: "\ninvalid_json\n", + wantErr: "Failed to parse", + }, + { + name: "test_has_a2ui_parts_true", + action: "has_parts", + input: "Hello [] World", + wantHasParts: true, + }, + { + name: "test_has_a2ui_parts_false_no_tags", + action: "has_parts", + input: "Hello World", + wantHasParts: false, + }, + { + name: "test_has_a2ui_parts_false_only_open", + action: "has_parts", + input: "Hello World", + wantHasParts: false, + }, + { + name: "test_fix_payload_trailing_comma_list", + action: "fix_payload", + input: `[{"type": "Text", "text": "Hello"},]`, + wantPayload: []map[string]any{{"type": "Text", "text": "Hello"}}, + }, + { + name: "test_fix_payload_trailing_comma_object", + action: "fix_payload", + input: `{"type": "Text", "text": "Hello",}`, + wantPayload: []map[string]any{{"type": "Text", "text": "Hello"}}, + }, + { + name: "test_fix_payload_auto_wrap", + action: "fix_payload", + input: `{"type": "Text", "text": "Hello"}`, + wantPayload: []map[string]any{{"type": "Text", "text": "Hello"}}, + }, + { + name: "test_fix_payload_smart_quotes", + action: "fix_payload", + input: `{"type": “Text”, "other": "Value’s"}`, + wantPayload: []map[string]any{{"type": "Text", "other": "Value's"}}, + }, + { + name: "test_fix_payload_commas_in_strings", + action: "fix_payload", + input: `{"text": "Hello, world", "array": ["a,b", "c"]}`, + wantPayload: []map[string]any{{"text": "Hello, world", "array": []any{"a,b", "c"}}}, + }, + } +} diff --git a/agent_sdks/go/a2uistream/v08.go b/agent_sdks/go/a2uistream/v08.go new file mode 100644 index 0000000000..6282c3f1aa --- /dev/null +++ b/agent_sdks/go/a2uistream/v08.go @@ -0,0 +1,350 @@ +package a2uistream + +import ( + "encoding/json" + "fmt" + "io" + "strings" +) + +// V08Part is a segment of a v0.8 response. +type V08Part struct { + Text string + Messages []map[string]any +} + +// V08Parser incrementally parses legacy A2UI v0.8 response chunks. +type V08Parser struct { + buf strings.Builder + inTag bool + jsonBuf strings.Builder + rootID string + surfaceID string + components map[string]map[string]any +} + +// V08Reader parses v0.8 response parts from an io.Reader. +type V08Reader struct { + r io.Reader + parser *V08Parser + buf []byte + queue []V08Part + done bool +} + +// NewV08Parser creates a legacy v0.8 streaming parser. +func NewV08Parser() *V08Parser { + return &V08Parser{components: make(map[string]map[string]any)} +} + +// NewV08Reader creates a v0.8 parser that reads from r. +func NewV08Reader(r io.Reader) *V08Reader { + return &V08Reader{ + r: r, + parser: NewV08Parser(), + buf: make([]byte, 32*1024), + } +} + +// Next returns the next parsed v0.8 response part. +func (r *V08Reader) Next() (V08Part, error) { + for len(r.queue) == 0 { + if r.done { + return V08Part{}, io.EOF + } + n, err := r.r.Read(r.buf) + if n > 0 { + parts, parseErr := r.parser.ProcessChunk(string(r.buf[:n])) + if parseErr != nil { + return V08Part{}, parseErr + } + r.queue = append(r.queue, parts...) + } + if err != nil { + if err != io.EOF { + return V08Part{}, err + } + r.done = true + parts, parseErr := r.parser.Flush() + if parseErr != nil { + return V08Part{}, parseErr + } + r.queue = append(r.queue, parts...) + } + } + part := r.queue[0] + copy(r.queue, r.queue[1:]) + r.queue = r.queue[:len(r.queue)-1] + return part, nil +} + +// ProcessChunk feeds a response chunk and returns complete v0.8 parts. +func (p *V08Parser) ProcessChunk(chunk string) ([]V08Part, error) { + p.buf.WriteString(chunk) + return p.drain() +} + +// Flush returns remaining text. +func (p *V08Parser) Flush() ([]V08Part, error) { + if p.inTag && p.jsonBuf.Len() > 0 { + parts, err := p.extractObjects() + if err != nil { + return nil, err + } + p.inTag = false + p.jsonBuf.Reset() + if p.buf.Len() > 0 { + text := p.buf.String() + p.buf.Reset() + parts = append(parts, V08Part{Text: text}) + } + return parts, nil + } + if p.buf.Len() == 0 { + return nil, nil + } + text := p.buf.String() + p.buf.Reset() + return []V08Part{{Text: text}}, nil +} + +func (p *V08Parser) drain() ([]V08Part, error) { + var parts []V08Part + for { + if !p.inTag { + found, done := p.scanV08Open(&parts) + if done || !found { + break + } + } + if p.inTag { + found, err := p.scanV08Close(&parts) + if err != nil { + return parts, err + } + if !found { + break + } + } + } + return parts, nil +} + +func (p *V08Parser) scanV08Open(parts *[]V08Part) (bool, bool) { + s := p.buf.String() + idx := strings.Index(s, openTag) + if idx >= 0 { + if idx > 0 { + *parts = append(*parts, V08Part{Text: s[:idx]}) + } + p.buf.Reset() + p.buf.WriteString(s[idx+len(openTag):]) + p.inTag = true + return true, false + } + keepLen := partialSuffix(s, openTag) + if safeLen := len(s) - keepLen; safeLen > 0 { + *parts = append(*parts, V08Part{Text: s[:safeLen]}) + p.buf.Reset() + p.buf.WriteString(s[safeLen:]) + } + return false, true +} + +func (p *V08Parser) scanV08Close(parts *[]V08Part) (bool, error) { + s := p.buf.String() + idx := strings.Index(s, closeTag) + if idx >= 0 { + p.jsonBuf.WriteString(s[:idx]) + jsonParts, err := p.extractObjects() + if err != nil { + return false, err + } + *parts = append(*parts, jsonParts...) + p.inTag = false + p.jsonBuf.Reset() + p.buf.Reset() + p.buf.WriteString(s[idx+len(closeTag):]) + return true, nil + } + keepLen := partialSuffix(s, closeTag) + if safeLen := len(s) - keepLen; safeLen > 0 { + p.jsonBuf.WriteString(s[:safeLen]) + p.buf.Reset() + p.buf.WriteString(s[safeLen:]) + jsonParts, err := p.extractObjects() + if err != nil { + return false, err + } + *parts = append(*parts, jsonParts...) + } + return false, nil +} + +func (p *V08Parser) extractObjects() ([]V08Part, error) { + raw := p.jsonBuf.String() + var parts []V08Part + consumed := 0 + depth := 0 + inStr := false + esc := false + start := -1 + for i, c := range raw { + if inStr { + if esc { + esc = false + continue + } + switch c { + case '\\': + esc = true + case '"': + inStr = false + } + continue + } + switch c { + case '"': + inStr = true + case '{': + if depth == 0 { + start = i + } + depth++ + case '}': + depth-- + if depth == 0 && start >= 0 { + var msg map[string]any + if err := json.Unmarshal([]byte(raw[start:i+1]), &msg); err != nil { + return nil, err + } + part, err := p.handleV08Message(msg) + if err != nil { + return nil, err + } + if len(part.Messages) > 0 { + parts = append(parts, part) + } + consumed = i + 1 + start = -1 + } + } + } + if consumed > 0 { + p.jsonBuf.Reset() + p.jsonBuf.WriteString(raw[consumed:]) + } + return parts, nil +} + +func (p *V08Parser) handleV08Message(msg map[string]any) (V08Part, error) { + if begin, ok := msg["beginRendering"].(map[string]any); ok { + p.surfaceID, _ = begin["surfaceId"].(string) + p.rootID, _ = begin["root"].(string) + return V08Part{Messages: []map[string]any{msg}}, nil + } + if update, ok := msg["surfaceUpdate"].(map[string]any); ok { + if sid, _ := update["surfaceId"].(string); sid != "" { + p.surfaceID = sid + } + for _, comp := range v08Components(update["components"]) { + if id, _ := comp["id"].(string); id != "" { + p.components[id] = comp + } + } + if p.rootID == "" { + return V08Part{}, nil + } + reachable, err := p.reachableComponents() + if err != nil { + return V08Part{}, err + } + if len(reachable) == 0 { + return V08Part{}, nil + } + return V08Part{Messages: []map[string]any{{"surfaceUpdate": map[string]any{ + "surfaceId": p.surfaceID, + "components": reachable, + }}}}, nil + } + if _, ok := msg["dataModelUpdate"]; ok { + return V08Part{Messages: []map[string]any{msg}}, nil + } + if _, ok := msg["deleteSurface"]; ok { + return V08Part{Messages: []map[string]any{msg}}, nil + } + return V08Part{}, nil +} + +func (p *V08Parser) reachableComponents() ([]map[string]any, error) { + seen := make(map[string]bool) + stack := make(map[string]bool) + var out []map[string]any + var visit func(string) error + visit = func(id string) error { + if stack[id] { + if id == p.rootID && len(stack) == 1 { + return fmt.Errorf("self-reference detected") + } + return fmt.Errorf("circular reference detected") + } + if seen[id] { + return nil + } + comp := p.components[id] + if comp == nil { + return nil + } + stack[id] = true + for _, ref := range v08Refs(comp) { + if err := visit(ref); err != nil { + return err + } + } + stack[id] = false + seen[id] = true + out = append(out, comp) + return nil + } + if err := visit(p.rootID); err != nil { + return nil, err + } + return out, nil +} + +func v08Components(v any) []map[string]any { + items, _ := v.([]any) + out := make([]map[string]any, 0, len(items)) + for _, item := range items { + if comp, ok := item.(map[string]any); ok { + out = append(out, comp) + } + } + return out +} + +func v08Refs(comp map[string]any) []string { + body, _ := comp["component"].(map[string]any) + var refs []string + for _, raw := range body { + obj, _ := raw.(map[string]any) + if child, _ := obj["child"].(string); child != "" { + refs = append(refs, child) + } + if children, ok := obj["children"].(map[string]any); ok { + refs = append(refs, stringSlice(children["explicitList"])...) + } + } + return refs +} + +func stringSlice(v any) []string { + items, _ := v.([]any) + out := make([]string, 0, len(items)) + for _, item := range items { + if s, ok := item.(string); ok { + out = append(out, s) + } + } + return out +} diff --git a/agent_sdks/go/a2uistream/v08_test.go b/agent_sdks/go/a2uistream/v08_test.go new file mode 100644 index 0000000000..9f6d883c36 --- /dev/null +++ b/agent_sdks/go/a2uistream/v08_test.go @@ -0,0 +1,91 @@ +package a2uistream + +import ( + "reflect" + "strings" + "testing" +) + +func TestV08IncrementalYielding(t *testing.T) { + p := NewV08Parser() + assertV08Parts(t, processV08(t, p, "Here is your"), []V08Part{{Text: "Here is your"}}) + assertV08Parts(t, processV08(t, p, " response.["), []V08Part{{Text: " response."}}) + got := processV08(t, p, `{"beginRendering": {"surfaceId": "s1", "root": "root-column"}},`) + assertV08Parts(t, got, []V08Part{{Messages: []map[string]any{{"beginRendering": map[string]any{"surfaceId": "s1", "root": "root-column"}}}}}) +} + +func TestV08ReachableComponents(t *testing.T) { + p := NewV08Parser() + assertV08Parts(t, processV08(t, p, `[{"beginRendering": {"root": "root", "surfaceId": "s1"}},`), []V08Part{ + {Messages: []map[string]any{{"beginRendering": map[string]any{"root": "root", "surfaceId": "s1"}}}}, + }) + if got := processV08(t, p, `{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "c1", "component": {"Text": {"text": {"literalString": "hello"}}}}`); len(got) != 0 { + t.Fatalf("partial update yielded %#v, want none", got) + } + got := processV08(t, p, `, {"id": "root", "component": {"Card": {"child": "c1"}}}]}}`) + want := []V08Part{{Messages: []map[string]any{{"surfaceUpdate": map[string]any{ + "surfaceId": "s1", + "components": []map[string]any{ + {"id": "c1", "component": map[string]any{"Text": map[string]any{"text": map[string]any{"literalString": "hello"}}}}, + {"id": "root", "component": map[string]any{"Card": map[string]any{"child": "c1"}}}, + }, + }}}}} + assertV08Parts(t, got, want) +} + +func TestV08IgnoresOrphanComponent(t *testing.T) { + p := NewV08Parser() + processV08(t, p, `[{"beginRendering": {"root": "root", "surfaceId": "s1"}}, `) + got := processV08(t, p, `{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "root", "component": {"Text": {"text": "root"}}}, {"id": "orphan", "component": {"Text": {"text": "orphan"}}}]}}] `) + want := []V08Part{{Messages: []map[string]any{{"surfaceUpdate": map[string]any{ + "surfaceId": "s1", + "components": []map[string]any{ + {"id": "root", "component": map[string]any{"Text": map[string]any{"text": "root"}}}, + }, + }}}}} + assertV08Parts(t, got, want) +} + +func TestV08CircularReferenceDetection(t *testing.T) { + p := NewV08Parser() + processV08(t, p, `[{"beginRendering": {"root": "c1", "surfaceId": "s1"}},`) + _, err := p.ProcessChunk(`{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "c1", "component": {"Card": {"child": "c2"}}}]}},{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "c2", "component": {"Card": {"child": "c1"}}}]}}]}} `) + if err == nil || !strings.Contains(err.Error(), "circular reference detected") { + t.Fatalf("err = %v, want circular reference", err) + } +} + +func TestV08SplitTagHandling(t *testing.T) { + p := NewV08Parser() + assertV08Parts(t, processV08(t, p, "Talking "); len(got) != 0 { + t.Fatalf("split tag yielded %#v, want none", got) + } + got := processV08(t, p, `[{"beginRendering": {"root": "r", "surfaceId": "s"}}] End.`) + want := []V08Part{ + {Messages: []map[string]any{{"beginRendering": map[string]any{"root": "r", "surfaceId": "s"}}}}, + {Text: " End."}, + } + assertV08Parts(t, got, want) +} + +func mustV08(t *testing.T, parts []V08Part, err error) []V08Part { + t.Helper() + if err != nil { + t.Fatal(err) + } + return parts +} + +func processV08(t *testing.T, p *V08Parser, chunk string) []V08Part { + t.Helper() + parts, err := p.ProcessChunk(chunk) + return mustV08(t, parts, err) +} + +func assertV08Parts(t *testing.T, got, want []V08Part) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("parts = %#v, want %#v", got, want) + } +} diff --git a/agent_sdks/go/a2uistream/validate.go b/agent_sdks/go/a2uistream/validate.go new file mode 100644 index 0000000000..c0f7d670b5 --- /dev/null +++ b/agent_sdks/go/a2uistream/validate.go @@ -0,0 +1,33 @@ +package a2uistream + +import "github.com/a2ui-project/a2ui/agent_sdks/go/a2ui" + +// MessageValidator validates a batch of parsed A2UI messages. +type MessageValidator interface { + ValidateMessages([]a2ui.ServerMessage) error +} + +// ParseAndValidate parses a complete response and validates each discovered message batch. +func ParseAndValidate(content string, validator MessageValidator) ([]ResponsePart, error) { + parser := NewParser() + parts, err := parser.ProcessChunk(content) + if err != nil { + return nil, err + } + flush, err := parser.Flush() + if err != nil { + return nil, err + } + parts = append(parts, flush...) + if validator != nil { + for _, part := range parts { + if len(part.Messages) == 0 { + continue + } + if err := validator.ValidateMessages(part.Messages); err != nil { + return nil, err + } + } + } + return parts, nil +} diff --git a/agent_sdks/go/cmd/a2uigen/main_test.go b/agent_sdks/go/cmd/a2uigen/main_test.go index 3a89613aa7..2ea7fdd908 100644 --- a/agent_sdks/go/cmd/a2uigen/main_test.go +++ b/agent_sdks/go/cmd/a2uigen/main_test.go @@ -3,6 +3,7 @@ package main import ( "os" "path/filepath" + "strings" "testing" ) @@ -62,3 +63,46 @@ func TestResolveOutputConfigExplicitA2UIImport(t *testing.T) { t.Fatalf("VersionImport = %q, want example.com/custom/a2ui/v010", cfg.VersionImport) } } + +func TestGenerateSDKRootLayout(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/root\n\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + + if err := generateSDK(dir, "example.com/root", ".", "a2uibuild", "", "", ""); err != nil { + t.Fatal(err) + } + + checks := []struct { + path string + want string + }{ + {"a2ui.go", `import "example.com/root/v09"`}, + {filepath.Join("a2uibuild", "zz_builders.go"), `import "example.com/root"`}, + {filepath.Join("a2uischema", "manager.go"), `"example.com/root/v010"`}, + } + for _, check := range checks { + data, err := os.ReadFile(filepath.Join(dir, check.path)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), check.want) { + t.Fatalf("%s does not contain %q", check.path, check.want) + } + } + + for _, path := range []string{ + filepath.Join(dir, "a2ui.go"), + filepath.Join(dir, "a2uischema", "manager.go"), + filepath.Join(dir, "a2uibuild", "surface.go"), + } { + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), "github.com/a2ui-project/a2ui/agent_sdks/go") { + t.Fatalf("%s contains upstream module import", path) + } + } +}