Skip to content

Commit 4093a8a

Browse files
authored
feat: Add merged skill source proxy. (#747)
This proxy allows combining multiple skill sources into a single source.
1 parent df03967 commit 4093a8a

2 files changed

Lines changed: 517 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package skill
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"io"
22+
"slices"
23+
)
24+
25+
type mergedSource struct {
26+
sources []Source
27+
}
28+
29+
// NewMergedSource creates a Source that combines multiple underlying
30+
// Source implementations. The sources are queried in the order they are
31+
// provided.
32+
func NewMergedSource(sources ...Source) Source {
33+
return &mergedSource{sources: slices.Clone(sources)}
34+
}
35+
36+
func (m *mergedSource) ListFrontmatters(ctx context.Context) ([]*Frontmatter, error) {
37+
var allFrontmatters []*Frontmatter
38+
names := make(map[string]bool) // Track skill names to detect duplicates.
39+
40+
for _, source := range m.sources {
41+
frontmatters, err := source.ListFrontmatters(ctx)
42+
if err != nil {
43+
return nil, err
44+
}
45+
for _, fm := range frontmatters {
46+
if names[fm.Name] {
47+
return nil, fmt.Errorf("%w: %q", ErrDuplicateSkill, fm.Name)
48+
}
49+
names[fm.Name] = true
50+
}
51+
allFrontmatters = append(allFrontmatters, frontmatters...)
52+
}
53+
54+
return allFrontmatters, nil
55+
}
56+
57+
func (m *mergedSource) ListResources(ctx context.Context, name, subpath string) ([]string, error) {
58+
for _, source := range m.sources {
59+
res, err := source.ListResources(ctx, name, subpath)
60+
if err == nil {
61+
return res, nil
62+
}
63+
if !errors.Is(err, ErrSkillNotFound) {
64+
return nil, err
65+
}
66+
}
67+
return nil, ErrSkillNotFound
68+
}
69+
70+
func (m *mergedSource) LoadFrontmatter(ctx context.Context, name string) (*Frontmatter, error) {
71+
for _, source := range m.sources {
72+
frontmatter, err := source.LoadFrontmatter(ctx, name)
73+
if err == nil {
74+
return frontmatter, nil
75+
}
76+
if !errors.Is(err, ErrSkillNotFound) {
77+
return nil, err
78+
}
79+
}
80+
return nil, ErrSkillNotFound
81+
}
82+
83+
func (m *mergedSource) LoadInstructions(ctx context.Context, name string) (string, error) {
84+
for _, source := range m.sources {
85+
instructions, err := source.LoadInstructions(ctx, name)
86+
if err == nil {
87+
return instructions, nil
88+
}
89+
if !errors.Is(err, ErrSkillNotFound) {
90+
return "", err
91+
}
92+
}
93+
return "", ErrSkillNotFound
94+
}
95+
96+
func (m *mergedSource) LoadResource(ctx context.Context, name, resourcePath string) (io.ReadCloser, error) {
97+
for _, source := range m.sources {
98+
resource, err := source.LoadResource(ctx, name, resourcePath)
99+
if err == nil {
100+
return resource, nil
101+
}
102+
if !errors.Is(err, ErrSkillNotFound) {
103+
return nil, err
104+
}
105+
}
106+
return nil, ErrSkillNotFound
107+
}

0 commit comments

Comments
 (0)