Files
zeekay d42c416b4f the address is the composer's, not this package's
The four routes were written at the literal /v1/agent, which is fine for the
standalone daemon and wrong for anyone else: a host whose router already spends
that address had no way to take this surface at all. It now composes off one
prefix — MountAt(app, prefix, ...) registers there, Mount(app, ...) is
MountAt at DefaultPrefix, so the daemon is unchanged and the host says where.

A prefix that is not an absolute path is refused at mount rather than
registering routes nothing can reach, and a trailing slash is the same address.
Nothing in the round reads the prefix, so the doc comments that named the old
literal now name the route.
2026-08-20 20:21:19 -07:00

152 lines
5.6 KiB
Go

package agent
import (
"encoding/json"
"sort"
"strings"
openai "github.com/hanzoai/go-openai"
)
// Preset is one named agent type in the preset LIBRARY — a first-class, extensible
// catalog the round is created from and the presets route lists.
// It frames a tool-calling round: the system prompt that instructs the model, the
// builtin tool defs offered alongside the caller's and the org's registered tools,
// and whether the model's tool calls are EXECUTED server-side (registry Dispatch →
// actions) or handed back to the client as ops (a graph/UI mutation the server
// cannot perform). Adding an agent type is one Register call — no other change.
type Preset struct {
ID string `json:"id"`
Title string `json:"title"`
SystemPrompt string `json:"systemPrompt"`
BuiltinTools []openai.Tool `json:"-"`
// ServerExecuted gates the tool-call split: when true, a call the tool
// registry knows is dispatched server-side and reported in actions; when
// false the round is advisory — every call is returned as an op for the
// client to apply.
ServerExecuted bool `json:"serverExecuted"`
}
// DefaultPreset is used when a request omits preset (and its capability alias).
const DefaultPreset = "graph"
// presets is the library. Seeded with graph + create below; a host extends it by
// calling Register before Mount.
var presets = map[string]Preset{}
// Register adds (or replaces) a preset in the library. Call from init() or before
// Mount. Replacing an existing id is allowed so a host can re-skin a builtin.
func Register(p Preset) {
presets[strings.TrimSpace(p.ID)] = p
}
// Presets returns every registered preset, sorted by ID for a stable listing.
func Presets() []Preset {
out := make([]Preset, 0, len(presets))
for _, p := range presets {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
// presetFor resolves a preset by id, defaulting an empty id to DefaultPreset. ok
// is false for an unknown id so the handler answers 400 rather than guessing.
func presetFor(id string) (Preset, bool) {
id = strings.TrimSpace(id)
if id == "" {
id = DefaultPreset
}
p, ok := presets[id]
return p, ok
}
// toolDef builds an OpenAI function tool from a JSON-Schema string. The schema is
// held as json.RawMessage so it reaches the model verbatim.
func toolDef(name, desc, schema string) openai.Tool {
return openai.Tool{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: name,
Description: desc,
Parameters: json.RawMessage(schema),
},
}
}
func init() {
Register(Preset{
ID: "graph",
Title: "Studio Graph Copilot",
ServerExecuted: false,
SystemPrompt: strings.TrimSpace(`
You are Hanzo Studio Copilot. You read and mutate a node workflow graph
(LiteGraph / ComfyUI-compatible). The user describes an outcome; you respond with
tool calls that describe the graph operations to apply. You never execute the
graph yourself — the Studio client applies each operation and re-queues the graph.
Reference nodes you create by the id you give them in add_node. Prefer the fewest
operations that achieve the request; explain briefly in text when helpful.`),
BuiltinTools: []openai.Tool{
toolDef("add_node", "Add a node to the graph.", `{
"type":"object",
"properties":{
"id":{"type":"string","description":"caller label to reference this node in later ops"},
"type":{"type":"string","description":"node class, e.g. KSampler"},
"pos":{"type":"array","items":{"type":"number"},"description":"[x,y] canvas position"}
},
"required":["type"]
}`),
toolDef("set_widget", "Set a widget value on a node.", `{
"type":"object",
"properties":{
"node_id":{"type":"string"},
"name":{"type":"string","description":"widget name"},
"value":{}
},
"required":["node_id","name","value"]
}`),
toolDef("set_prompt", "Set the prompt text on a node.", `{
"type":"object",
"properties":{"node_id":{"type":"string"},"text":{"type":"string"}},
"required":["node_id","text"]
}`),
toolDef("connect", "Connect an output slot to an input slot.", `{
"type":"object",
"properties":{
"from":{"type":"string","description":"source node id"},
"from_slot":{"description":"output slot index or name"},
"to":{"type":"string","description":"target node id"},
"to_slot":{"description":"input slot index or name"}
},
"required":["from","to"]
}`),
toolDef("move_node", "Move a node to a new position.", `{
"type":"object",
"properties":{"node_id":{"type":"string"},"pos":{"type":"array","items":{"type":"number"}}},
"required":["node_id","pos"]
}`),
toolDef("delete_node", "Remove a node from the graph.", `{
"type":"object",
"properties":{"node_id":{"type":"string"}},
"required":["node_id"]
}`),
toolDef("layout", "Auto-arrange the graph.", `{"type":"object","properties":{}}`),
toolDef("queue", "Queue the graph for execution.", `{"type":"object","properties":{}}`),
},
})
Register(Preset{
ID: "create",
Title: "Product & Fashion Create",
ServerExecuted: true,
SystemPrompt: strings.TrimSpace(`
You are Hanzo Create, a fashion and product render assistant. You help the user
generate, edit, and refine product and fashion imagery. Use the tools available to
this workspace — the org's connected render and MCP services — to run and iterate
on renders. Call a tool when it advances the request; describe the result briefly.`),
// No builtin tools: the callable set is the org's registered MCP/registry
// tools, resolved per request and dispatched server-side.
BuiltinTools: nil,
})
}