Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Runnable examples for common [HOL Standards SDK](https://hol.org/docs/libraries/
| Example | Specification | Description |
| ------- | ------------- | ----------- |
| `hcs2-create-registry` | [HCS-2](https://hol.org/docs/standards/hcs-2) | Creates an indexed topic registry |
| `registry-broker-delegation` | Registry Broker | Calls `Delegate` and prints ranked delegation opportunities |
| `hcs5-build-mint` | [HCS-5](https://hol.org/docs/standards/hcs-5) | Builds a Hashinal mint transaction |
| `hcs6-create-registry` | [HCS-6](https://hol.org/docs/standards/hcs-6) | Creates a dynamic hashinal registry |
| `hcs7-register-metadata` | [HCS-7](https://hol.org/docs/standards/hcs-7) | Registers metadata in an indexed registry |
Expand All @@ -19,8 +20,8 @@ Runnable examples for common [HOL Standards SDK](https://hol.org/docs/libraries/
| `hcs20-deploy-points` | [HCS-20](https://hol.org/docs/standards/hcs-20) | Deploys a private points topic |
| `hcs21-build-declaration` | [HCS-21](https://hol.org/docs/standards/hcs-21) | Builds an adapter declaration |
| `hcs26-parse-memos` | [HCS-26](https://hol.org/docs/standards/hcs-26) | Parses topic/transaction memos |
| `hcs27-publish-checkpoint` | [HCS-27](https://hol.org/docs/standards/hcs-27) | Publishes inline + HRL-backed checkpoints and validates the chain |
| `inscriber-auth-client` | [HCS-1](https://hol.org/docs/standards/hcs-1) | Authenticates with the inscriber service |
| `hcs27-publish-checkpoint` | [HCS-27](https://hol.org/docs/standards/hcs-27) | Publishes a Merkle checkpoint |
| `inscriber-auth-client` | [HCS-1](https://hol.org/docs/standards/hcs-1) | Authenticates with Kiloscribe |
| `registry-broker-skill-domain-proof` | [HCS-26](https://hol.org/docs/standards/hcs-26) | Creates/verifies DNS TXT proof and checks trust delta |
| `registry-broker-uaid-dns-verification` | [HCS-14](https://hol.org/docs/standards/hcs-14) | Verifies UAID DNS TXT binding and reads stored/live verification status |

Expand All @@ -33,6 +34,7 @@ go run ./examples/hcs2-create-registry
## CodeSandbox

- [`hcs2-create-registry`](https://codesandbox.io/s/github/hashgraph-online/standards-sdk-go/tree/main/examples/hcs2-create-registry)
- [`registry-broker-delegation`](https://codesandbox.io/s/github/hashgraph-online/standards-sdk-go/tree/main/examples/registry-broker-delegation)
- [`hcs5-build-mint`](https://codesandbox.io/s/github/hashgraph-online/standards-sdk-go/tree/main/examples/hcs5-build-mint)
- [`hcs6-create-registry`](https://codesandbox.io/s/github/hashgraph-online/standards-sdk-go/tree/main/examples/hcs6-create-registry)
- [`hcs7-register-metadata`](https://codesandbox.io/s/github/hashgraph-online/standards-sdk-go/tree/main/examples/hcs7-register-metadata)
Expand Down
20 changes: 20 additions & 0 deletions examples/registry-broker-delegation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Registry Broker Delegation

This example calls the broker-native delegation planner and prints the ranked
opportunities plus the top candidate per opportunity.

Optional environment variables:

```bash
export REGISTRY_BROKER_BASE_URL="https://hol.org/registry/api/v1"
export REGISTRY_BROKER_API_KEY="..."
export REGISTRY_BROKER_DELEGATION_TASK="Review an SDK PR and split out docs and verification subtasks."
export REGISTRY_BROKER_DELEGATION_CONTEXT="Need a docs-focused sidecar pass."
export REGISTRY_BROKER_DELEGATION_LIMIT="3"
```

## Run

```bash
go run ./examples/registry-broker-delegation
```
77 changes: 77 additions & 0 deletions examples/registry-broker-delegation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"context"
"fmt"
"os"
"strconv"
"strings"

"github.com/hashgraph-online/standards-sdk-go/pkg/registrybroker"
)

const (
defaultBaseURL = "https://hol.org/registry/api/v1"
defaultTask = "Review an SDK PR and split out docs and verification subtasks."
)

func main() {
if err := run(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
Comment thread
kantorcodes marked this conversation as resolved.
}

func run() error {
client, err := registrybroker.NewRegistryBrokerClient(registrybroker.RegistryBrokerClientOptions{
BaseURL: strings.TrimSpace(getEnvOrDefault("REGISTRY_BROKER_BASE_URL", defaultBaseURL)),
APIKey: strings.TrimSpace(os.Getenv("REGISTRY_BROKER_API_KEY")),
})
if err != nil {
return err
}

limit, err := strconv.Atoi(strings.TrimSpace(getEnvOrDefault("REGISTRY_BROKER_DELEGATION_LIMIT", "3")))
if err != nil || limit <= 0 {
limit = 3
}

response, err := client.Delegate(context.Background(), registrybroker.DelegationPlanRequest{
Task: strings.TrimSpace(getEnvOrDefault("REGISTRY_BROKER_DELEGATION_TASK", defaultTask)),
Context: strings.TrimSpace(os.Getenv("REGISTRY_BROKER_DELEGATION_CONTEXT")),
Limit: limit,
})
if err != nil {
return err
}

fmt.Printf("task=%s\n", response.Task)
fmt.Printf("shouldDelegate=%t\n", response.ShouldDelegate)
if response.LocalFirstReason != "" {
fmt.Printf("localFirstReason=%s\n", response.LocalFirstReason)
}
for index := range response.Opportunities {
opportunity := &response.Opportunities[index]
fmt.Printf("\nopportunity=%s title=%s\n", opportunity.ID, opportunity.Title)
fmt.Printf("reason=%s\n", opportunity.Reason)
if len(opportunity.Candidates) == 0 {
fmt.Println("topCandidate=<none>")
continue
}
candidate := opportunity.Candidates[0]
label := candidate.Label
if label == "" {
label = "<unknown>"
}
fmt.Printf("topCandidate=%s label=%s\n", candidate.UAID, label)
}
return nil
}

func getEnvOrDefault(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
27 changes: 27 additions & 0 deletions examples/registry-broker-delegation/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import "testing"

func TestGetEnvOrDefault(t *testing.T) {
t.Run("returns fallback when env var is not set", func(t *testing.T) {
if value := getEnvOrDefault("REGISTRY_BROKER_DELEGATION_EXAMPLE_MISSING", "fallback"); value != "fallback" {
t.Fatalf("expected fallback, got %s", value)
}
})

t.Run("returns env var value when set", func(t *testing.T) {
t.Setenv("REGISTRY_BROKER_DELEGATION_EXAMPLE_SET", "env-value")

if value := getEnvOrDefault("REGISTRY_BROKER_DELEGATION_EXAMPLE_SET", "fallback"); value != "env-value" {
t.Fatalf("expected env-value, got %s", value)
}
})

t.Run("returns trimmed env var value when set with whitespace", func(t *testing.T) {
t.Setenv("REGISTRY_BROKER_DELEGATION_EXAMPLE_SPACED", " env-value ")

if value := getEnvOrDefault("REGISTRY_BROKER_DELEGATION_EXAMPLE_SPACED", "fallback"); value != "env-value" {
t.Fatalf("expected env-value, got %s", value)
}
})
}
Comment thread
kantorcodes marked this conversation as resolved.
134 changes: 134 additions & 0 deletions pkg/registrybroker/delegation_plan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package registrybroker

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)

const preservedValue = "preserved"

func TestDelegate(t *testing.T) {
t.Helper()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Fatalf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/delegate" {
t.Fatalf("expected /api/v1/delegate, got %s", r.URL.Path)
}

var request DelegationPlanRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err)
}
if request.Task != "Review SDK PR feedback" {
t.Fatalf("expected task to round-trip, got %q", request.Task)
}
if request.Filter == nil || len(request.Filter.Protocols) != 1 || request.Filter.Protocols[0] != "mcp" {
t.Fatalf("expected protocols filter to round-trip, got %#v", request.Filter)
}

w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{
"task": "Review SDK PR feedback",
"context": "Need a docs-focused sidecar pass.",
"summary": "Delegate documentation follow-up.",
"shouldDelegate": true,
"localFirstReason": "Main agent owns the implementation work.",
"recommendation": { "summary": "Delegate docs only", "mode": "parallel" },
"opportunities": [
{
"id": "docs",
"title": "Docs follow-up",
"reason": "Bounded copy update",
"role": "docs",
"type": "sidecar",
"suggestedMode": "parallel",
"searchQueries": ["docs markdown docusaurus"],
"extraOpportunityField": "%s",
"candidates": [
{
"uaid": "uaid-1",
"label": "Docs Agent",
"registry": "hcs-11",
"score": 0.98,
"trustScore": 0.91,
"verified": true,
"communicationSupported": true,
"availability": "online",
"explanation": "Strong docs match",
"matchedRoles": ["docs"],
"reasons": ["Strong docs match"],
"suggestedMessage": "Update the docs tab set.",
"extraCandidateField": "%s",
"agent": {
"name": "Docs Agent",
"verified": true,
"extraAgentField": "%s"
}
}
]
}
],
"extraRootField": "%s"
}`, preservedValue, preservedValue, preservedValue, preservedValue)
}))
defer server.Close()

client, err := NewRegistryBrokerClient(RegistryBrokerClientOptions{BaseURL: server.URL})
if err != nil {
t.Fatalf("new client: %v", err)
}

response, err := client.Delegate(context.Background(), DelegationPlanRequest{
Task: "Review SDK PR feedback",
Context: "Need a docs-focused sidecar pass.",
Limit: 2,
Filter: &DelegationPlanFilter{
Protocols: []string{"mcp"},
Type: "mcp_server",
},
Workspace: JSONObject{"repo": "hashgraph-online/standards-sdk"},
})
if err != nil {
t.Fatalf("delegate: %v", err)
}

if !response.ShouldDelegate {
t.Fatal("expected delegation recommendation")
}
if response.Context != "Need a docs-focused sidecar pass." {
t.Fatalf("expected context to parse, got %q", response.Context)
}
if len(response.Opportunities) != 1 {
t.Fatalf("expected one opportunity, got %d", len(response.Opportunities))
}
if response.Extras["extraRootField"] != preservedValue {
t.Fatalf("expected additive root field to survive, got %#v", response.Extras)
}
opportunity := response.Opportunities[0]
if opportunity.Extras["extraOpportunityField"] != preservedValue {
t.Fatalf("expected additive opportunity field to survive, got %#v", opportunity.Extras)
}
candidate := opportunity.Candidates[0]
if candidate.Registry != "hcs-11" {
t.Fatalf("expected registry to parse, got %q", candidate.Registry)
}
if candidate.TrustScore != 0.91 {
t.Fatalf("expected trust score 0.91, got %f", candidate.TrustScore)
}
if candidate.Verified == nil || !*candidate.Verified {
t.Fatal("expected verified candidate")
}
if candidate.Extras["extraCandidateField"] != preservedValue {
t.Fatalf("expected additive candidate field to survive, got %#v", candidate.Extras)
}
if candidate.Agent["extraAgentField"] != preservedValue {
t.Fatalf("expected additive agent field to survive, got %#v", candidate.Agent)
}
}
34 changes: 34 additions & 0 deletions pkg/registrybroker/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package registrybroker

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand All @@ -15,6 +16,39 @@ func (c *RegistryBrokerClient) Search(ctx context.Context, params SearchParams)
return c.requestJSON(ctx, http.MethodGet, path, nil, nil)
}

// Delegate returns a typed delegation plan for the requested task.
func (c *RegistryBrokerClient) Delegate(
ctx context.Context,
request DelegationPlanRequest,
) (DelegationPlanResponse, error) {
var result DelegationPlanResponse

rawBody, rawHeaders, err := c.request(
ctx,
http.MethodPost,
"/delegate",
request,
map[string]string{"content-type": "application/json"},
)
if err != nil {
return result, err
}
if !isJSONContentType(rawHeaders.Get("content-type")) {
return result, &RegistryBrokerParseError{
Message: "expected JSON response from registry broker",
Body: strings.TrimSpace(string(rawBody)),
}
}
if err := json.Unmarshal(rawBody, &result); err != nil {
return result, &RegistryBrokerParseError{
Message: "failed to decode registry broker response",
Body: strings.TrimSpace(string(rawBody)),
Cause: err,
}
}
return result, nil
}

// SearchErc8004ByAgentID performs the requested operation.
func (c *RegistryBrokerClient) SearchErc8004ByAgentID(
ctx context.Context,
Expand Down
Loading
Loading