Recipe: define an in-process tool
Register a plain Go function as a Harbor tool. The in-process driver derives JSON Schemas from your typed input/output structs by reflection and wraps the function in the ToolPolicy reliability shell (timeout + retry + validation) — a plain registration is production-resilient by construction.
The full runnable version of this recipe is examples/tools/weather/.
Steps
Declare typed input and output structs. JSON Schema is derived from the
jsonstruct tags:gotype LookupArgs struct { City string `json:"city"` } type LookupResult struct { City string `json:"city"` TempC float64 `json:"temp_c"` Summary string `json:"summary"` }Write the tool body. It MUST be safe to invoke concurrently (CLAUDE.md §5, D-025) — hold no mutable state, read only from the arguments and
ctx. Honour context cancellation:gofunc lookup(ctx context.Context, in LookupArgs) (LookupResult, error) { if err := ctx.Err(); err != nil { return LookupResult{}, fmt.Errorf("weather.lookup: %w", err) } if in.City == "" { return LookupResult{}, fmt.Errorf("weather.lookup: city is required") } return LookupResult{City: in.City, TempC: 21.0, Summary: "clear skies"}, nil }Register it against a catalog with
inproc.RegisterFunc. The import paths are the publicsdk/facade (RFC §3.6) — they work identically from inside this module and from an external Go module:goimport ( "github.com/hurtener/Harbor/sdk/tools" "github.com/hurtener/Harbor/sdk/tools/inproc" ) cat := tools.NewCatalog() err := inproc.RegisterFunc[LookupArgs, LookupResult]( cat, "weather.lookup", lookup, tools.WithDescription("Look up current weather conditions by city name."), tools.WithTags("example", "weather"), tools.WithSideEffect(tools.SideEffectExternal), )Common
DescriptorOptions (see thesdk/toolsgodoc):WithDescription,WithTags,WithAuthScopes,WithSideEffect,WithExamples,WithCostHint,WithLatencyHint,WithPolicy.Resolve and invoke (this is what a planner does internally):
godesc, ok := cat.Resolve("weather.lookup") // desc.Validate(args) runs the derived schema validator; // desc.Invoke(ctx, args) runs the policy-wrapped function.
Notes
RegisterFuncfails loudly on a duplicate name or an unrepresentable type — never silently. Check the returned error.- Heavy outputs (≥ the configured
heavy_output_threshold_bytes) route through the ArtifactStore automatically (D-022, D-026); your tool returns typed values, not blobs. - Heavy inputs go the other way, by reference: declare a field of type
sdk/tools.ArtifactRefand the model supplies an artifact id as a plain string while the runtime resolves it at dispatch and hands your function the bytes (in.Doc.Bytes()). The model never sees the content, and the resolved value never reaches the trajectory, an event payload or a log (D-354). In-process tools only. Worked example:examples/tools/artifactstats/. - HTTP, MCP, and A2A tools use different transport drivers but the same catalog surface; they are wired declaratively from the
toolsblock inharbor.yaml(no Go call site) — seeexamples/harbor.yaml.