Skip to content

Commit ce2748b

Browse files
authored
Merge branch 'main' into fix/result-cache-deadlock-623
2 parents be28fdb + 054a7d1 commit ce2748b

25 files changed

Lines changed: 593 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,94 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66

77
<!-- changelog:entries -->
88

9+
## [0.1.113] - 2026-07-21
10+
11+
## [0.1.113-rc.1] - 2026-07-21
12+
13+
14+
### Added
15+
16+
- Feat: node-declared reasoner descriptions + entry points in discovery and af ls (#805)
17+
18+
* feat(control-plane): per-reasoner descriptions + entrypoint surfacing in discovery and catalog
19+
20+
ReasonerDefinition/SkillDefinition gain a description field (rides in the
21+
reasoners JSON blob — no migration). Discovery prefers the record-level
22+
description and falls back to the legacy agent metadata map; the serverless
23+
ingest path stops dropping the description it already parsed. The reasoner
24+
catalog (af ls backend) returns description+tags, supports entrypoints=true,
25+
and sorts entrypoint-tagged rows first among never-run rows. Adds
26+
types.TagEntrypoint as the shared tag convention.
27+
28+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29+
30+
* feat(cli): af ls shows reasoner descriptions and supports --entrypoints
31+
32+
Rows render a trailing description column ([entrypoint]-labeled when the
33+
reasoner carries the entrypoint tag, truncated to one line); -e/--entrypoints
34+
filters to entry points so a caller browsing a node sees its intended surface.
35+
36+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
37+
38+
* feat(sdk-go): transmit reasoner descriptions to the control plane
39+
40+
WithDescription was captured locally for CLI help but never sent at
41+
registration. ReasonerDefinition gains the description field and registerNode
42+
copies it, so discovery and af ls can surface it.
43+
44+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45+
46+
* feat(sdk-python): description kwarg on @reasoner/@skill with docstring default
47+
48+
@app.reasoner(description=...) / @app.skill(description=...) register a
49+
caller-facing summary with the control plane; without it, the first paragraph
50+
of the function docstring (whitespace-collapsed) is used. Routers forward the
51+
kwarg through include_router unchanged. Payloads without a description are
52+
byte-identical to before, so older control planes are unaffected.
53+
54+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
55+
56+
---------
57+
58+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (075dd32)
59+
60+
## [0.1.112] - 2026-07-21
61+
62+
## [0.1.112-rc.3] - 2026-07-21
63+
64+
65+
### Fixed
66+
67+
- Fix: prevent exception details in agent server responses (#800)
68+
69+
Co-authored-by: Abir Abbas <abirabbas1998@gmail.com> (26c8428)
70+
71+
## [0.1.112-rc.2] - 2026-07-21
72+
73+
74+
### Chores
75+
76+
- Chore(deps): bump axios (#802)
77+
78+
Bumps the npm_and_yarn group with 1 update in the /sdk/typescript directory: [axios](https://github.com/axios/axios).
79+
80+
81+
Updates `axios` from 1.16.0 to 1.18.0
82+
- [Release notes](https://github.com/axios/axios/releases)
83+
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
84+
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.0)
85+
86+
---
87+
updated-dependencies:
88+
- dependency-name: axios
89+
dependency-version: 1.18.0
90+
dependency-type: direct:production
91+
dependency-group: npm_and_yarn
92+
...
93+
94+
Signed-off-by: dependabot[bot] <support@github.com>
95+
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (4867040)
96+
997
## [0.1.112-rc.1] - 2026-07-20
1098

1199

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.112-rc.1
1+
0.1.113

control-plane/internal/cli/ls_reasoners.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type lsOptions struct {
1717
all bool
1818
node string
1919
live bool
20+
entrypoints bool
2021
outputFormat string
2122
stdout io.Writer
2223
stdoutTTY bool
@@ -29,10 +30,12 @@ type reasonerListResponse struct {
2930
}
3031

3132
type reasonerListItem struct {
32-
Node string `json:"node"`
33-
Reasoner string `json:"reasoner"`
34-
LastRunAt *string `json:"last_run_at,omitempty"`
35-
Status string `json:"status"`
33+
Node string `json:"node"`
34+
Reasoner string `json:"reasoner"`
35+
Description string `json:"description,omitempty"`
36+
Tags []string `json:"tags,omitempty"`
37+
LastRunAt *string `json:"last_run_at,omitempty"`
38+
Status string `json:"status"`
3639
}
3740

3841
func NewReasonerListCommand() *cobra.Command {
@@ -57,6 +60,7 @@ func NewReasonerListCommand() *cobra.Command {
5760
cmd.Flags().BoolVar(&opts.all, "all", false, "Show every reasoner")
5861
cmd.Flags().StringVar(&opts.node, "node", "", "Filter to a single node")
5962
cmd.Flags().BoolVar(&opts.live, "live", false, "Only show reasoners whose node is live")
63+
cmd.Flags().BoolVarP(&opts.entrypoints, "entrypoints", "e", false, "Only show reasoners tagged as entry points")
6064
cmd.Flags().StringVarP(&opts.outputFormat, "output", "o", "", "Output format: pretty, json, yaml")
6165
return cmd
6266
}
@@ -82,6 +86,9 @@ func runReasonerList(ctx context.Context, query string, opts *lsOptions) error {
8286
if opts.live {
8387
values.Set("live", "true")
8488
}
89+
if opts.entrypoints {
90+
values.Set("entrypoints", "true")
91+
}
8592
resp, err := makeRequest(ctx, http.MethodGet, appendQuery("/api/v1/reasoners", values), nil, "application/json")
8693
if err != nil {
8794
return cliExitError{Code: 3, Err: err}
@@ -107,13 +114,39 @@ func renderReasonerList(out io.Writer, resp reasonerListResponse, recentHeader b
107114
}
108115
for _, item := range resp.Reasoners {
109116
name := item.Node + "." + item.Reasoner
110-
fmt.Fprintf(out, "%-28s %-12s %s\n", name, relativeTime(item.LastRunAt), item.Status)
117+
fmt.Fprintf(out, "%-28s %-12s %-6s %s\n", name, relativeTime(item.LastRunAt), item.Status, describeReasoner(item))
111118
}
112119
if resp.Total > resp.Shown {
113120
fmt.Fprintf(out, "\n%d more recent - %d total - use `af ls --all`\n", resp.Total-resp.Shown, resp.Total)
114121
}
115122
}
116123

124+
// describeReasoner renders the trailing description column: entry points are
125+
// labeled so callers can spot a node's intended surface, and long descriptions
126+
// are truncated to keep rows on one line.
127+
func describeReasoner(item reasonerListItem) string {
128+
const maxLen = 80
129+
desc := strings.Join(strings.Fields(item.Description), " ")
130+
if len(desc) > maxLen {
131+
desc = desc[:maxLen-1] + "…"
132+
}
133+
entry := false
134+
for _, tag := range item.Tags {
135+
if strings.EqualFold(strings.TrimSpace(tag), "entrypoint") {
136+
entry = true
137+
break
138+
}
139+
}
140+
switch {
141+
case entry && desc != "":
142+
return "[entrypoint] " + desc
143+
case entry:
144+
return "[entrypoint]"
145+
default:
146+
return desc
147+
}
148+
}
149+
117150
func relativeTime(value *string) string {
118151
if value == nil || strings.TrimSpace(*value) == "" {
119152
return "-"

control-plane/internal/handlers/discovery.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func buildDiscoveryResponse(agents []*types.AgentNode, filters DiscoveryFilters)
517517
reasonerCap.OutputSchema = decodeSchema(reasoner.OutputSchema)
518518
}
519519
if filters.IncludeDescriptions {
520-
reasonerCap.Description = extractDescription(agent.Metadata, reasoner.ID)
520+
reasonerCap.Description = reasonerDescription(agent.Metadata, reasoner)
521521
}
522522
if filters.IncludeExamples {
523523
reasonerCap.Examples = extractExamples(agent.Metadata, reasoner.ID)
@@ -544,7 +544,11 @@ func buildDiscoveryResponse(agents []*types.AgentNode, filters DiscoveryFilters)
544544
skillCap.InputSchema = decodeSchema(skill.InputSchema)
545545
}
546546
if filters.IncludeDescriptions {
547-
skillCap.Description = extractDescription(agent.Metadata, skill.ID)
547+
if desc := strings.TrimSpace(skill.Description); desc != "" {
548+
skillCap.Description = &desc
549+
} else {
550+
skillCap.Description = extractDescription(agent.Metadata, skill.ID)
551+
}
548552
}
549553

550554
capability.Skills = append(capability.Skills, skillCap)
@@ -595,6 +599,16 @@ func decodeSchema(raw json.RawMessage) map[string]interface{} {
595599
return schema
596600
}
597601

602+
// reasonerDescription resolves a reasoner's description: the field the SDK
603+
// registered on the reasoner record wins; the legacy agent-level
604+
// metadata.Custom["descriptions"] map remains as a fallback for older SDKs.
605+
func reasonerDescription(metadata types.AgentMetadata, reasoner types.ReasonerDefinition) *string {
606+
if desc := strings.TrimSpace(reasoner.Description); desc != "" {
607+
return &desc
608+
}
609+
return extractDescription(metadata, reasoner.ID)
610+
}
611+
598612
func extractDescription(metadata types.AgentMetadata, id string) *string {
599613
if metadata.Custom == nil {
600614
return nil

control-plane/internal/handlers/discovery_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,45 @@ func TestDiscoveryCapabilities_WithFiltersAndSchemas(t *testing.T) {
9696
assert.Empty(t, resp.Capabilities[0].Skills)
9797
}
9898

99+
// Validation contract: a description registered on the reasoner/skill record
100+
// itself (new SDKs) must win over the legacy agent-level metadata map, and
101+
// records without one must still fall back to that map (old SDKs).
102+
func TestDiscoveryCapabilities_RecordLevelDescriptions(t *testing.T) {
103+
gin.SetMode(gin.TestMode)
104+
InvalidateDiscoveryCache()
105+
106+
agents := buildDiscoveryAgents()
107+
// agent-alpha's reasoner+skill gain record-level descriptions that differ
108+
// from the metadata map entries; agent-beta stays metadata-map-only.
109+
agents[0].Reasoners[0].Description = "Record-level reasoner description"
110+
agents[0].Skills[0].Description = "Record-level skill description"
111+
112+
lister := &stubAgentLister{agents: agents}
113+
router := gin.New()
114+
router.GET("/api/v1/discovery/capabilities", DiscoveryCapabilitiesHandler(lister))
115+
116+
req := httptest.NewRequest(http.MethodGet, "/api/v1/discovery/capabilities", nil)
117+
rec := httptest.NewRecorder()
118+
router.ServeHTTP(rec, req)
119+
120+
require.Equal(t, http.StatusOK, rec.Code)
121+
var resp DiscoveryResponse
122+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
123+
require.Len(t, resp.Capabilities, 2)
124+
125+
alpha := resp.Capabilities[0]
126+
require.Equal(t, "agent-alpha", alpha.AgentID)
127+
require.NotNil(t, alpha.Reasoners[0].Description)
128+
assert.Equal(t, "Record-level reasoner description", *alpha.Reasoners[0].Description)
129+
require.NotNil(t, alpha.Skills[0].Description)
130+
assert.Equal(t, "Record-level skill description", *alpha.Skills[0].Description)
131+
132+
beta := resp.Capabilities[1]
133+
require.Equal(t, "agent-beta", beta.AgentID)
134+
require.NotNil(t, beta.Reasoners[0].Description)
135+
assert.Equal(t, "Performs comprehensive research", *beta.Reasoners[0].Description)
136+
}
137+
99138
func TestDiscoveryCapabilities_Formats(t *testing.T) {
100139
gin.SetMode(gin.TestMode)
101140
InvalidateDiscoveryCache()

control-plane/internal/handlers/nodes_register.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,7 @@ func RegisterServerlessAgentHandler(storageProvider storage.StorageProvider, uiS
930930
outputSchemaBytes, _ := json.Marshal(r.OutputSchema)
931931
reasoners[i] = types.ReasonerDefinition{
932932
ID: r.ID,
933+
Description: r.Description,
933934
InputSchema: json.RawMessage(inputSchemaBytes),
934935
OutputSchema: json.RawMessage(outputSchemaBytes),
935936
Tags: r.Tags,
@@ -946,6 +947,7 @@ func RegisterServerlessAgentHandler(storageProvider storage.StorageProvider, uiS
946947
inputSchemaBytes, _ := json.Marshal(s.InputSchema)
947948
skills[i] = types.SkillDefinition{
948949
ID: s.ID,
950+
Description: s.Description,
949951
InputSchema: json.RawMessage(inputSchemaBytes),
950952
Tags: s.Tags,
951953
}

control-plane/internal/handlers/reasoner_catalog.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ type ReasonerCatalogStore interface {
2222
}
2323

2424
type ReasonerCatalogRow struct {
25-
Node string `json:"node"`
26-
Reasoner string `json:"reasoner"`
27-
LastRunAt *string `json:"last_run_at,omitempty"`
28-
Status string `json:"status"`
25+
Node string `json:"node"`
26+
Reasoner string `json:"reasoner"`
27+
Description string `json:"description,omitempty"`
28+
Tags []string `json:"tags,omitempty"`
29+
LastRunAt *string `json:"last_run_at,omitempty"`
30+
Status string `json:"status"`
2931
}
3032

3133
type ReasonerCatalogResponse struct {
@@ -70,6 +72,7 @@ func ListReasonersHandler(store ReasonerCatalogStore) gin.HandlerFunc {
7072
nodeFilter := strings.TrimSpace(c.Query("node"))
7173
liveOnly := parseQueryBool(c.Query("live"))
7274
showAll := parseQueryBool(c.Query("all"))
75+
entrypointsOnly := parseQueryBool(c.Query("entrypoints"))
7376

7477
rows := make([]ReasonerCatalogRow, 0)
7578
for _, agent := range agents {
@@ -84,6 +87,9 @@ func ListReasonersHandler(store ReasonerCatalogStore) gin.HandlerFunc {
8487
continue
8588
}
8689
for _, reasoner := range agent.Reasoners {
90+
if entrypointsOnly && !hasTag(reasoner.Tags, types.TagEntrypoint) {
91+
continue
92+
}
8793
fullName := agent.ID + "." + reasoner.ID
8894
if query != "" && !fuzzyReasonerMatch(fullName, query) {
8995
continue
@@ -93,11 +99,19 @@ func ListReasonersHandler(store ReasonerCatalogStore) gin.HandlerFunc {
9399
formatted := ts.Format(time.RFC3339)
94100
lastRun = &formatted
95101
}
102+
description := strings.TrimSpace(reasoner.Description)
103+
if description == "" {
104+
if fromMeta := extractDescription(agent.Metadata, reasoner.ID); fromMeta != nil {
105+
description = strings.TrimSpace(*fromMeta)
106+
}
107+
}
96108
rows = append(rows, ReasonerCatalogRow{
97-
Node: agent.ID,
98-
Reasoner: reasoner.ID,
99-
LastRunAt: lastRun,
100-
Status: status,
109+
Node: agent.ID,
110+
Reasoner: reasoner.ID,
111+
Description: description,
112+
Tags: reasoner.Tags,
113+
LastRunAt: lastRun,
114+
Status: status,
101115
})
102116
}
103117
}
@@ -111,6 +125,13 @@ func ListReasonersHandler(store ReasonerCatalogStore) gin.HandlerFunc {
111125
if leftOK && !left.Equal(right) {
112126
return left.After(right)
113127
}
128+
// Among never-run (or same-timestamp) rows, surface declared entry
129+
// points first so a fresh registry lists callable surfaces on top.
130+
leftEntry := hasTag(rows[i].Tags, types.TagEntrypoint)
131+
rightEntry := hasTag(rows[j].Tags, types.TagEntrypoint)
132+
if leftEntry != rightEntry {
133+
return leftEntry
134+
}
114135
return rows[i].Node+"."+rows[i].Reasoner < rows[j].Node+"."+rows[j].Reasoner
115136
})
116137

@@ -256,6 +277,15 @@ func reasonerNodeStatus(agent *types.AgentNode) string {
256277
}
257278
}
258279

280+
func hasTag(tags []string, want string) bool {
281+
for _, tag := range tags {
282+
if strings.EqualFold(strings.TrimSpace(tag), want) {
283+
return true
284+
}
285+
}
286+
return false
287+
}
288+
259289
func fuzzyReasonerMatch(value, query string) bool {
260290
value = strings.ToLower(value)
261291
query = strings.ToLower(strings.TrimSpace(query))

0 commit comments

Comments
 (0)