feat(locale): enhance locale handling and agent functionalities
- Added locale support in various components, including the Assistant's execution context and agent tools, to improve internationalization. - Updated the `pickNodeByFilter` function to include a fallback mechanism for host_exec nodes when no container nodes are available. - Introduced a new `agent_reference` tool for downloading agent source code for read-only study, alongside updates to the `agent_download` tool to restrict downloads to the 'smith' namespace. - Enhanced documentation and tests to reflect the new functionalities and ensure robust locale extraction and handling.
This commit is contained in:
parent
639f0c59fc
commit
da4a803c11
16 changed files with 319 additions and 19 deletions
|
|
@ -235,6 +235,7 @@ func (ast *Assistant) executeSandboxV2Stream(
|
||||||
Token: tok,
|
Token: tok,
|
||||||
Logger: ctx.Logger,
|
Logger: ctx.Logger,
|
||||||
UserExplicit: p.Options != nil && p.Options.Connector != "",
|
UserExplicit: p.Options != nil && p.Options.Connector != "",
|
||||||
|
Locale: ctx.Locale,
|
||||||
}
|
}
|
||||||
|
|
||||||
execReq := &sandboxv2.ExecuteRequest{
|
execReq := &sandboxv2.ExecuteRequest{
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,10 @@ func buildEnv(req *types.StreamRequest, p platform) map[string]string {
|
||||||
}
|
}
|
||||||
env["WORKDIR"] = workDir
|
env["WORKDIR"] = workDir
|
||||||
|
|
||||||
|
if req.Locale != "" {
|
||||||
|
env["CTX_LOCALE"] = req.Locale
|
||||||
|
}
|
||||||
|
|
||||||
assistantID := req.AssistantID
|
assistantID := req.AssistantID
|
||||||
if assistantID != "" {
|
if assistantID != "" {
|
||||||
configDir := p.PathJoin(workDir, ".yao", "assistants", assistantID)
|
configDir := p.PathJoin(workDir, ".yao", "assistants", assistantID)
|
||||||
|
|
|
||||||
|
|
@ -329,8 +329,9 @@ func resolveOwnerID(ctx *agentContext.Context) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// pickNodeByFilter selects a random online node that satisfies the given filter
|
// pickNodeByFilter selects a random online node that satisfies the given filter
|
||||||
// and image requirement. If image is non-empty, candidate nodes must have a
|
// and image requirement. If image is non-empty, nodes with a container runtime
|
||||||
// container runtime (Docker or K8s).
|
// (Docker or K8s) are preferred; if none are available, host_exec nodes are
|
||||||
|
// accepted as fallback (ResolveNodeID will resolve them to host mode).
|
||||||
func pickNodeByFilter(filter *types.ComputerFilter, image string) (string, error) {
|
func pickNodeByFilter(filter *types.ComputerFilter, image string) (string, error) {
|
||||||
reg := registry.Global()
|
reg := registry.Global()
|
||||||
if reg == nil {
|
if reg == nil {
|
||||||
|
|
@ -339,6 +340,7 @@ func pickNodeByFilter(filter *types.ComputerFilter, image string) (string, error
|
||||||
|
|
||||||
nodes := reg.List()
|
nodes := reg.List()
|
||||||
var candidates []string
|
var candidates []string
|
||||||
|
var hostExecFallback []string
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.Status != "online" && n.Status != "" {
|
if n.Status != "online" && n.Status != "" {
|
||||||
continue
|
continue
|
||||||
|
|
@ -372,12 +374,20 @@ func pickNodeByFilter(filter *types.ComputerFilter, image string) (string, error
|
||||||
}
|
}
|
||||||
|
|
||||||
if image != "" && !(n.Capabilities.Docker || n.Capabilities.K8s) {
|
if image != "" && !(n.Capabilities.Docker || n.Capabilities.K8s) {
|
||||||
|
if n.Capabilities.HostExec {
|
||||||
|
hostExecFallback = append(hostExecFallback, n.TaiID)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates = append(candidates, n.TaiID)
|
candidates = append(candidates, n.TaiID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 && len(hostExecFallback) > 0 {
|
||||||
|
log.Trace("[sandbox/v2] pickNodeByFilter: no container node for image %q, falling back to host_exec node", image)
|
||||||
|
candidates = hostExecFallback
|
||||||
|
}
|
||||||
|
|
||||||
if len(candidates) == 0 {
|
if len(candidates) == 0 {
|
||||||
kind := ""
|
kind := ""
|
||||||
os := ""
|
os := ""
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,5 @@ type StreamRequest struct {
|
||||||
Token *SandboxToken // current user's sandbox token for MCP callbacks
|
Token *SandboxToken // current user's sandbox token for MCP callbacks
|
||||||
Logger *agentContext.RequestLogger // request-scoped logger propagated from agent context
|
Logger *agentContext.RequestLogger // request-scoped logger propagated from agent context
|
||||||
UserExplicit bool // true when the user explicitly selected the primary connector
|
UserExplicit bool // true when the user explicitly selected the primary connector
|
||||||
|
Locale string // user locale (e.g. "zh-cn", "en-us") for i18n in MCP tools
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ func authProviderFromCtx(ctx context.Context) *grpcAuthProvider {
|
||||||
if ids := md.Get("x-sandbox-id"); len(ids) > 0 && ids[0] != "" {
|
if ids := md.Get("x-sandbox-id"); len(ids) > 0 && ids[0] != "" {
|
||||||
m["sandbox_id"] = ids[0]
|
m["sandbox_id"] = ids[0]
|
||||||
}
|
}
|
||||||
|
if vals := md.Get("x-locale"); len(vals) > 0 && vals[0] != "" {
|
||||||
|
m["locale"] = vals[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &grpcAuthProvider{m: m}
|
return &grpcAuthProvider{m: m}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ var DownloadSchemaJSON []byte
|
||||||
//go:embed deploy_schema.json
|
//go:embed deploy_schema.json
|
||||||
var DeploySchemaJSON []byte
|
var DeploySchemaJSON []byte
|
||||||
|
|
||||||
|
//go:embed reference_schema.json
|
||||||
|
var ReferenceSchemaJSON []byte
|
||||||
|
|
||||||
//go:embed connectors_schema.json
|
//go:embed connectors_schema.json
|
||||||
var ConnectorsSchemaJSON []byte
|
var ConnectorsSchemaJSON []byte
|
||||||
|
|
||||||
|
|
@ -68,6 +71,21 @@ func extractWorkspaceID(proc *process.Process) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractLocale(proc *process.Process) string {
|
||||||
|
if proc.Context == nil {
|
||||||
|
return "en-us"
|
||||||
|
}
|
||||||
|
md, ok := metadata.FromIncomingContext(proc.Context)
|
||||||
|
if !ok {
|
||||||
|
return "en-us"
|
||||||
|
}
|
||||||
|
vals := md.Get("x-locale")
|
||||||
|
if len(vals) > 0 && vals[0] != "" {
|
||||||
|
return strings.ToLower(vals[0])
|
||||||
|
}
|
||||||
|
return "en-us"
|
||||||
|
}
|
||||||
|
|
||||||
func validateID(id string) error {
|
func validateID(id string) error {
|
||||||
if strings.Contains(id, "..") {
|
if strings.Contains(id, "..") {
|
||||||
return fmt.Errorf("invalid id: path traversal not allowed")
|
return fmt.Errorf("invalid id: path traversal not allowed")
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@ func TestSchemaJSON_NonEmpty(t *testing.T) {
|
||||||
schemas := map[string][]byte{
|
schemas := map[string][]byte{
|
||||||
"ListSchemaJSON": ListSchemaJSON,
|
"ListSchemaJSON": ListSchemaJSON,
|
||||||
"DownloadSchemaJSON": DownloadSchemaJSON,
|
"DownloadSchemaJSON": DownloadSchemaJSON,
|
||||||
|
"ReferenceSchemaJSON": ReferenceSchemaJSON,
|
||||||
"DeploySchemaJSON": DeploySchemaJSON,
|
"DeploySchemaJSON": DeploySchemaJSON,
|
||||||
"ConnectorsSchemaJSON": ConnectorsSchemaJSON,
|
"ConnectorsSchemaJSON": ConnectorsSchemaJSON,
|
||||||
}
|
}
|
||||||
|
|
@ -396,12 +397,65 @@ func TestDownloadHandler_InvalidID(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloadHandler_WrongNamespace(t *testing.T) {
|
||||||
|
proc := &process.Process{Args: []interface{}{"yao.slides"}}
|
||||||
|
result := DownloadHandler(proc)
|
||||||
|
m := result.(map[string]interface{})
|
||||||
|
errMsg, has := m["error"]
|
||||||
|
if !has {
|
||||||
|
t.Fatal("expected error for non-smith namespace")
|
||||||
|
}
|
||||||
|
if !contains(errMsg.(string), "smith") {
|
||||||
|
t.Errorf("error = %q, want substring 'smith'", errMsg)
|
||||||
|
}
|
||||||
|
if !contains(errMsg.(string), "agent_reference") {
|
||||||
|
t.Errorf("error = %q, should suggest agent_reference", errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloadHandler_MissingWorkspace(t *testing.T) {
|
func TestDownloadHandler_MissingWorkspace(t *testing.T) {
|
||||||
|
proc := &process.Process{
|
||||||
|
Args: []interface{}{"smith.test"},
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
result := DownloadHandler(proc)
|
||||||
|
m := result.(map[string]interface{})
|
||||||
|
errMsg, has := m["error"]
|
||||||
|
if !has {
|
||||||
|
t.Fatal("expected error when workspace_id is missing")
|
||||||
|
}
|
||||||
|
if !contains(errMsg.(string), "workspace_id") {
|
||||||
|
t.Errorf("error = %q, want substring 'workspace_id'", errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceHandler_MissingID(t *testing.T) {
|
||||||
|
proc := &process.Process{Args: []interface{}{""}}
|
||||||
|
result := ReferenceHandler(proc)
|
||||||
|
m := result.(map[string]interface{})
|
||||||
|
if _, has := m["error"]; !has {
|
||||||
|
t.Error("expected error for empty id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceHandler_InvalidID(t *testing.T) {
|
||||||
|
cases := []string{"no/slash", "a..b", ""}
|
||||||
|
for _, id := range cases {
|
||||||
|
proc := &process.Process{Args: []interface{}{id}}
|
||||||
|
result := ReferenceHandler(proc)
|
||||||
|
m := result.(map[string]interface{})
|
||||||
|
if _, has := m["error"]; !has {
|
||||||
|
t.Errorf("ReferenceHandler(%q) expected error", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceHandler_MissingWorkspace(t *testing.T) {
|
||||||
proc := &process.Process{
|
proc := &process.Process{
|
||||||
Args: []interface{}{"yao.slides"},
|
Args: []interface{}{"yao.slides"},
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
result := DownloadHandler(proc)
|
result := ReferenceHandler(proc)
|
||||||
m := result.(map[string]interface{})
|
m := result.(map[string]interface{})
|
||||||
errMsg, has := m["error"]
|
errMsg, has := m["error"]
|
||||||
if !has {
|
if !has {
|
||||||
|
|
@ -428,6 +482,57 @@ func TestDeployHandler_MissingWorkspace(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- extractLocale tests ---
|
||||||
|
|
||||||
|
func TestExtractLocale_WithMetadata(t *testing.T) {
|
||||||
|
md := metadata.Pairs("x-locale", "zh-cn")
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
proc := &process.Process{Context: ctx}
|
||||||
|
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
if locale != "zh-cn" {
|
||||||
|
t.Errorf("extractLocale = %q, want %q", locale, "zh-cn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractLocale_UpperCase(t *testing.T) {
|
||||||
|
md := metadata.Pairs("x-locale", "ZH-CN")
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
proc := &process.Process{Context: ctx}
|
||||||
|
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
if locale != "zh-cn" {
|
||||||
|
t.Errorf("extractLocale = %q, want %q (should lowercase)", locale, "zh-cn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractLocale_NoMetadata(t *testing.T) {
|
||||||
|
proc := &process.Process{Context: context.Background()}
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
if locale != "en-us" {
|
||||||
|
t.Errorf("extractLocale without metadata = %q, want default %q", locale, "en-us")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractLocale_NilContext(t *testing.T) {
|
||||||
|
proc := &process.Process{}
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
if locale != "en-us" {
|
||||||
|
t.Errorf("extractLocale with nil context = %q, want default %q", locale, "en-us")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractLocale_EmptyValue(t *testing.T) {
|
||||||
|
md := metadata.Pairs("x-locale", "")
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
proc := &process.Process{Context: ctx}
|
||||||
|
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
if locale != "en-us" {
|
||||||
|
t.Errorf("extractLocale with empty value = %q, want default %q", locale, "en-us")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
func contains(s, substr string) bool {
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,32 @@ package agent
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DownloadHandler handles the agent_download tool.
|
// DownloadHandler handles the agent_download tool.
|
||||||
// Args[0]: id (string, dot notation e.g. "yao.slides")
|
// Restricted to the smith namespace — used for downloading agents to edit.
|
||||||
|
// For read-only reference of other namespaces, use agent_reference instead.
|
||||||
|
// Args[0]: id (string, dot notation e.g. "smith.weather")
|
||||||
func DownloadHandler(proc *process.Process) interface{} {
|
func DownloadHandler(proc *process.Process) interface{} {
|
||||||
id := proc.ArgsString(0)
|
id := proc.ArgsString(0)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return map[string]interface{}{"error": "id is required (e.g. 'yao.slides')"}
|
return map[string]interface{}{"error": "id is required (e.g. 'smith.weather')"}
|
||||||
}
|
}
|
||||||
if err := validateID(id); err != nil {
|
if err := validateID(id); err != nil {
|
||||||
return map[string]interface{}{"error": err.Error()}
|
return map[string]interface{}{"error": err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(id, ".", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != allowedDeployNamespace {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"error": fmt.Sprintf("download restricted to '%s' namespace; use agent_reference for other agents", allowedDeployNamespace),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wsFS, err := resolveWorkspaceFS(proc)
|
wsFS, err := resolveWorkspaceFS(proc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]interface{}{"error": err.Error()}
|
return map[string]interface{}{"error": err.Error()}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "agent_download",
|
"name": "agent_download",
|
||||||
"description": "Download agent source code from the host into the sandbox development directory. Use this to study existing agents as reference. Any agent can be downloaded (read-only reference).",
|
"description": "Download a smith-namespace agent into the development directory for editing. Restricted to the 'smith' namespace only. For read-only reference of other agents, use agent_reference instead.",
|
||||||
"process": "tools.agent_download",
|
"process": "tools.agent_download",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Agent ID in dot notation (e.g. 'yao.slides', 'smith.weather')"
|
"description": "Agent ID in dot notation, must be smith namespace (e.g. 'smith.weather')"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["id"]
|
"required": ["id"]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
goufs "github.com/yaoapp/gou/fs"
|
goufs "github.com/yaoapp/gou/fs"
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListHandler handles the agent_list tool.
|
// ListHandler handles the agent_list tool.
|
||||||
|
|
@ -18,6 +19,8 @@ func ListHandler(proc *process.Process) interface{} {
|
||||||
namespace = proc.ArgsString(0)
|
namespace = proc.ArgsString(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
locale := extractLocale(proc)
|
||||||
|
|
||||||
app, err := goufs.Get("app")
|
app, err := goufs.Get("app")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]interface{}{"error": fmt.Sprintf("app filesystem: %s", err.Error())}
|
return map[string]interface{}{"error": fmt.Sprintf("app filesystem: %s", err.Error())}
|
||||||
|
|
@ -70,14 +73,74 @@ func ListHandler(proc *process.Process) interface{} {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name := pkg.Name
|
||||||
|
description := pkg.Description
|
||||||
|
capabilities := pkg.Capabilities
|
||||||
|
resolveLocaleFields(agentDir, locale, &name, &description, &capabilities)
|
||||||
|
|
||||||
agents = append(agents, agentInfo{
|
agents = append(agents, agentInfo{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: pkg.Name,
|
Name: name,
|
||||||
Description: pkg.Description,
|
Description: description,
|
||||||
Capabilities: pkg.Capabilities,
|
Capabilities: capabilities,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{"agents": agents}
|
return map[string]interface{}{"agents": agents}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveLocaleFields replaces {{ key }} templates in name/description using
|
||||||
|
// the agent's locales/ directory. Falls back gracefully: exact locale →
|
||||||
|
// language code (e.g. "zh") → en-us → raw template.
|
||||||
|
func resolveLocaleFields(agentDir, locale string, fields ...*string) {
|
||||||
|
hasTemplate := false
|
||||||
|
for _, f := range fields {
|
||||||
|
if strings.Contains(*f, "{{") {
|
||||||
|
hasTemplate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasTemplate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locales, err := i18n.GetLocales(agentDir)
|
||||||
|
if err != nil || len(locales) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
locales = locales.Flatten()
|
||||||
|
|
||||||
|
li := findLocale(locales, locale)
|
||||||
|
if li == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
if parsed := li.Parse(*f); parsed != nil {
|
||||||
|
if s, ok := parsed.(string); ok {
|
||||||
|
*f = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLocale(locales i18n.Map, locale string) *i18n.I18n {
|
||||||
|
locale = strings.ToLower(locale)
|
||||||
|
if li, ok := locales[locale]; ok {
|
||||||
|
return &li
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(locale, "-", 2)
|
||||||
|
if len(parts) > 1 {
|
||||||
|
if li, ok := locales[parts[0]]; ok {
|
||||||
|
return &li
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if li, ok := locales["en-us"]; ok {
|
||||||
|
return &li
|
||||||
|
}
|
||||||
|
if li, ok := locales["en"]; ok {
|
||||||
|
return &li
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
48
tools/agent/reference.go
Normal file
48
tools/agent/reference.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReferenceHandler handles the agent_reference tool.
|
||||||
|
// Downloads agent source code to .references/ for read-only study.
|
||||||
|
// Args[0]: id (string, dot notation e.g. "yao.slides")
|
||||||
|
func ReferenceHandler(proc *process.Process) interface{} {
|
||||||
|
id := proc.ArgsString(0)
|
||||||
|
if id == "" {
|
||||||
|
return map[string]interface{}{"error": "id is required (e.g. 'yao.slides')"}
|
||||||
|
}
|
||||||
|
if err := validateID(id); err != nil {
|
||||||
|
return map[string]interface{}{"error": err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
wsFS, err := resolveWorkspaceFS(proc)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]interface{}{"error": err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath := idToPath(id)
|
||||||
|
appRoot := config.Conf.Root
|
||||||
|
srcURI := "local:///" + filepath.Join(appRoot, "assistants", relPath)
|
||||||
|
dstPath := filepath.Join("agent-smith-dev", ".references", relPath)
|
||||||
|
|
||||||
|
result, copyErr := wsFS.Copy(srcURI, dstPath)
|
||||||
|
if copyErr != nil {
|
||||||
|
return map[string]interface{}{"error": fmt.Sprintf("reference download failed: %s", copyErr.Error())}
|
||||||
|
}
|
||||||
|
|
||||||
|
files := 0
|
||||||
|
if result != nil {
|
||||||
|
files = result.FilesSynced
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"path": dstPath,
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tools/agent/reference_schema.json
Normal file
16
tools/agent/reference_schema.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "agent_reference",
|
||||||
|
"description": "Download agent source code from the host into the .references/ directory for read-only study. Any agent across all namespaces can be downloaded. Use this to study existing agent patterns before building your own.",
|
||||||
|
"process": "tools.agent_reference",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Agent ID in dot notation (e.g. 'yao.slides', 'yao.keeper')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id"]
|
||||||
|
},
|
||||||
|
"x-process-args": ["$args.id"]
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"tools": {
|
"tools": {
|
||||||
"agent_list": "tools.agent_list",
|
"agent_list": "tools.agent_list",
|
||||||
"agent_download": "tools.agent_download",
|
"agent_download": "tools.agent_download",
|
||||||
|
"agent_reference": "tools.agent_reference",
|
||||||
"agent_deploy": "tools.agent_deploy",
|
"agent_deploy": "tools.agent_deploy",
|
||||||
"agent_connectors": "tools.agent_connectors"
|
"agent_connectors": "tools.agent_connectors"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ You have access to Yao system tools via the `tai` command in bash.
|
||||||
| `image_generate` | yao-image | Generate images from text prompts |
|
| `image_generate` | yao-image | Generate images from text prompts |
|
||||||
| `image_providers` | yao-image | List available image generation or vision providers |
|
| `image_providers` | yao-image | List available image generation or vision providers |
|
||||||
| `agent_list` | yao-agent | List available agents on the host |
|
| `agent_list` | yao-agent | List available agents on the host |
|
||||||
| `agent_download` | yao-agent | Download agent source code for reference |
|
| `agent_download` | yao-agent | Download smith agent for editing (smith only) |
|
||||||
|
| `agent_reference` | yao-agent | Download agent source to .references/ for study |
|
||||||
| `agent_deploy` | yao-agent | Deploy agent code to host (smith namespace only) |
|
| `agent_deploy` | yao-agent | Deploy agent code to host (smith namespace only) |
|
||||||
| `agent_connectors` | yao-agent | Get LLM connector matrix (no keys) |
|
| `agent_connectors` | yao-agent | Get LLM connector matrix (no keys) |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
name: yao-agent
|
name: yao-agent
|
||||||
description: Agent management expert. ALWAYS invoke this skill when you need to list available agents, download agent source code for reference, deploy agent code to the host, or query the LLM connector matrix. Do not guess agent structures — use this skill first.
|
description: Agent management expert. ALWAYS invoke this skill when you need to list available agents, download or reference agent source code, deploy agent code to the host, or query the LLM connector matrix. Do not guess agent structures — use this skill first.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Agent Tools
|
# Agent Tools
|
||||||
|
|
||||||
Four tools for managing agents on the host, called via bash.
|
Five tools for managing agents on the host, called via bash.
|
||||||
|
|
||||||
## agent_list
|
## agent_list
|
||||||
|
|
||||||
|
|
@ -22,16 +22,33 @@ tai tool agent_list '{"namespace": "smith"}'
|
||||||
|
|
||||||
## agent_download
|
## agent_download
|
||||||
|
|
||||||
Download agent source code from the host into `agent-smith-dev/assistants/` for reference. Any agent across all namespaces can be downloaded (read-only).
|
Download a **smith-namespace** agent into the development directory for editing. **Restricted to `smith` namespace only** — for other agents, use `agent_reference`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tai tool agent_download '{"id": "yao.slides"}'
|
tai tool agent_download '{"id": "smith.weather"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|----------------------------------------------------------|
|
||||||
|
| `id` | string | yes | Agent ID in dot notation. Must be `smith.*`. |
|
||||||
|
|
||||||
|
Downloaded code lands in `agent-smith-dev/assistants/smith/<name>/`.
|
||||||
|
|
||||||
|
## agent_reference
|
||||||
|
|
||||||
|
Download agent source code from the host into `.references/` for **read-only study**. Any agent across all namespaces can be referenced.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tai tool agent_reference '{"id": "yao.slides"}'
|
||||||
|
tai tool agent_reference '{"id": "yao.keeper"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|--------|----------|----------------------------------------------------|
|
|-----------|--------|----------|----------------------------------------------------|
|
||||||
| `id` | string | yes | Agent ID in dot notation (e.g. `yao.slides`) |
|
| `id` | string | yes | Agent ID in dot notation (e.g. `yao.slides`) |
|
||||||
|
|
||||||
|
Referenced code lands in `agent-smith-dev/.references/<namespace>/<name>/`.
|
||||||
|
|
||||||
## agent_deploy
|
## agent_deploy
|
||||||
|
|
||||||
Deploy agent source code from the sandbox development directory to the host. **Restricted to the `smith` namespace only** — attempts to deploy to other namespaces will be rejected.
|
Deploy agent source code from the sandbox development directory to the host. **Restricted to the `smith` namespace only** — attempts to deploy to other namespaces will be rejected.
|
||||||
|
|
@ -58,8 +75,9 @@ No parameters required.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Use `agent_list` to discover agents before downloading
|
- Use `agent_list` to discover agents before downloading or referencing
|
||||||
- Downloaded code lands in `agent-smith-dev/assistants/<namespace>/<name>/`
|
- `agent_download` is for editing smith agents — code lands in `agent-smith-dev/assistants/smith/<name>/`
|
||||||
|
- `agent_reference` is for studying any agent — code lands in `agent-smith-dev/.references/<namespace>/<name>/`
|
||||||
- Deploy is restricted to the `smith` namespace for safety
|
- Deploy is restricted to the `smith` namespace for safety
|
||||||
- Connector data never includes API keys, secrets, or tokens
|
- Connector data never includes API keys, secrets, or tokens
|
||||||
- All output is JSON
|
- All output is JSON
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ func init() {
|
||||||
"image_providers": image.ProvidersHandler,
|
"image_providers": image.ProvidersHandler,
|
||||||
"agent_list": agent.ListHandler,
|
"agent_list": agent.ListHandler,
|
||||||
"agent_download": agent.DownloadHandler,
|
"agent_download": agent.DownloadHandler,
|
||||||
|
"agent_reference": agent.ReferenceHandler,
|
||||||
"agent_deploy": agent.DeployHandler,
|
"agent_deploy": agent.DeployHandler,
|
||||||
"agent_connectors": agent.ConnectorsHandler,
|
"agent_connectors": agent.ConnectorsHandler,
|
||||||
})
|
})
|
||||||
|
|
@ -58,8 +59,8 @@ func init() {
|
||||||
registerMCPServer(mcpImageDSL, "yao-image",
|
registerMCPServer(mcpImageDSL, "yao-image",
|
||||||
image.ReadSchemaJSON, image.GenerateSchemaJSON, image.ProvidersSchemaJSON)
|
image.ReadSchemaJSON, image.GenerateSchemaJSON, image.ProvidersSchemaJSON)
|
||||||
registerMCPServer(mcpAgentDSL, "yao-agent",
|
registerMCPServer(mcpAgentDSL, "yao-agent",
|
||||||
agent.ListSchemaJSON, agent.DownloadSchemaJSON, agent.DeploySchemaJSON,
|
agent.ListSchemaJSON, agent.DownloadSchemaJSON, agent.ReferenceSchemaJSON,
|
||||||
agent.ConnectorsSchemaJSON)
|
agent.DeploySchemaJSON, agent.ConnectorsSchemaJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerMCPServer(dsl []byte, id string, schemas ...[]byte) {
|
func registerMCPServer(dsl []byte, id string, schemas ...[]byte) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue