feat(wire): 地基④ wire.go 接 vision/voice/mcp/skill/exec 五个子系统,从死代码变可用
- vision:model.Router 绑 WithVisionModel(LLM_VISION_MODEL);vision.RegisterTools(VisualTool/OCRTool) 用同一 OpenAICompatibleAdapter(LLM_BASE_URL 为空则跳过并 Warn)。 - voice:voice.New(cfg.Voice) Chain(cloud→local→edge-tts/espeak→silent)存 a.voice。 - mcp:读 cfg.MCP.ServersJSON → 逐 Enabled server mcp.NewRegistry(...).Register(ctx)(Connect+ DiscoverTools+RegisterTool),注册 KindMCP 能力 mcp:<name> 供 adminListPlugins 枚举; close() 中 Unregister 防 stdio 子进程孤儿;每 server 15s connect 超时,失败 Warn 跳过。 - skills:LoadDir(configs/skills) → skills.Register 为 KindSkill(Runner.Run 仍 ErrRunPending, 执行留后续)。 - exec/terminal:新 terminalTool(sdk.Tool) 内部调 execProv.Execute,注册进 dispatcher; ExecSandbox.Enabled=false 默认拦截(授权默认关)。 - config:LLMConfig 增 VisionModel(LLM_VISION_MODEL);新增 MCPConfig/ServersJSON(MCPSERVERS_JSON)。 - 拆 5 个辅助文件(vision.go/voice.go/mcp.go/skills.go/terminal_tool.go)避免 wire.go 膨胀; wire.go 最终 535 行。 默认配置下各子系统干净跳过不阻断启动(LLM_BASE_URL 已配则 vision 用主模型降级)。 go build/vet/test 全绿(46 包),gofmt 干净。
This commit is contained in:
1 parent
9b207a31e8
commit
eddb9b9891
8 files changed
+362
-1
No files matched your search
@@ -39,6 +39,10 @@ LLM_API_KEY=sk-xxxxx
|
|||||||
LLM_MODEL=gpt-4o
|
LLM_MODEL=gpt-4o
|
||||||
LLM_FALLBACK_MODEL=gpt-4o-mini
|
LLM_FALLBACK_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
|
# ---- LLM 视觉模型(多模态路由:含图片的请求路由到此模型) ----
|
||||||
|
# 不配置则图片路由回主模型;仅当启用多模态能力时设置。
|
||||||
|
# LLM_VISION_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
# Provider = anthropic:Anthropic Messages /v1/messages(tool_use.input 为结构化对象,工具循环更稳)
|
# Provider = anthropic:Anthropic Messages /v1/messages(tool_use.input 为结构化对象,工具循环更稳)
|
||||||
# ANTHROPIC_BASE_URL=https://api.anthropic.com
|
# ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||||
# ANTHROPIC_API_KEY=sk-ant-xxxxx
|
# ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||||
@@ -70,5 +74,18 @@ THINK_SILENCE_TIMEOUT_SEC=120
|
|||||||
WS_MAX_CONNECTIONS=1000
|
WS_MAX_CONNECTIONS=1000
|
||||||
SESSION_IDLE_TIMEOUT_MIN=30
|
SESSION_IDLE_TIMEOUT_MIN=30
|
||||||
|
|
||||||
|
# ---- MCP 服务器(可选,JSON 数组字符串;每台 server 含 name/command/args/url/enabled) ----
|
||||||
|
# stdio 子进程示例:
|
||||||
|
# MCP_SERVERS_JSON=[{"name":"calc","command":"mcp-calc","args":["--stdio"],"enabled":true}]
|
||||||
|
# SSE 端点示例:
|
||||||
|
# MCP_SERVERS_JSON=[{"name":"weather","url":"http://localhost:3001/sse","enabled":true}]
|
||||||
|
|
||||||
|
# ---- 本地执行沙箱(exec/terminal 工具;默认关闭,授权放权) ----
|
||||||
|
# 只有 Enabled=true 时 agent 才可执行命令;仍需命令白名单与根目录边界。
|
||||||
|
# EXEC_SANDBOX_ENABLED=false
|
||||||
|
# EXEC_SANDBOX_COMMANDS=git,ls,cat,pwd
|
||||||
|
# EXEC_SANDBOX_ROOTS=./data
|
||||||
|
# EXEC_SANDBOX_ENV=PATH
|
||||||
|
|
||||||
# ---- 跨域白名单 ----
|
# ---- 跨域白名单 ----
|
||||||
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/mcp"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mcpConnectTimeout bounds each MCP server's connect+discover handshake so a hung
|
||||||
|
// server degrades to a warning instead of blocking startup.
|
||||||
|
const mcpConnectTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// wireMCP reads cfg.MCP.ServersJSON, connects to every enabled server, registers
|
||||||
|
// its discovered tools into the capability registry (KindTool so the agent can
|
||||||
|
// call them), and surfaces a KindMCP capability so the admin plugin list
|
||||||
|
// enumerates the server. A server that fails to connect or is unconfigured
|
||||||
|
// degrades to a warning and never blocks startup.
|
||||||
|
func (a *app) wireMCP() {
|
||||||
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||||
|
servers, err := a.mcpServers()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("mcp config parse skipped", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
log.Debug("no mcp servers configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, srv := range servers {
|
||||||
|
if !srv.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout)
|
||||||
|
err := a.registerMCPServer(ctx, srv)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("mcp server skipped", "name", srv.Name, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mcpServers parses the configured MCP servers from cfg.MCP.ServersJSON.
|
||||||
|
func (a *app) mcpServers() ([]config.MCPServer, error) {
|
||||||
|
raw := strings.TrimSpace(a.cfg.MCP.ServersJSON)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var servers []config.MCPServer
|
||||||
|
if err := json.Unmarshal([]byte(raw), &servers); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerMCPServer connects to one server, registers its tools, and publishes a
|
||||||
|
// KindMCP capability under "mcp:<name>". It returns the first error so callers
|
||||||
|
// can classify it; on partial registration it cleans up what it added.
|
||||||
|
func (a *app) registerMCPServer(ctx context.Context, srv config.MCPServer) error {
|
||||||
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||||
|
reg := mcp.NewRegistry(a.caps, mcp.MCPConfig{
|
||||||
|
Name: srv.Name,
|
||||||
|
Command: srv.Command,
|
||||||
|
Args: srv.Args,
|
||||||
|
URL: srv.URL,
|
||||||
|
}, mcp.WithLogger(a.log))
|
||||||
|
if err := reg.Register(ctx); err != nil {
|
||||||
|
_ = reg.Unregister(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.caps.Register(registry.Capability{
|
||||||
|
Name: "mcp:" + srv.Name,
|
||||||
|
Kind: registry.KindMCP,
|
||||||
|
Description: "MCP server " + srv.Name,
|
||||||
|
Value: reg,
|
||||||
|
}); err != nil {
|
||||||
|
log.Warn("mcp capability register skipped", "name", srv.Name, "error", err)
|
||||||
|
}
|
||||||
|
a.mcpRegs = append(a.mcpRegs, reg)
|
||||||
|
log.Info("mcp server registered", "name", srv.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/skills"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultSkillsDir is the on-disk directory of skill YAML definitions. It is
|
||||||
|
// optional: a missing directory skips skill loading with a warning.
|
||||||
|
const defaultSkillsDir = "configs/skills"
|
||||||
|
|
||||||
|
// wireSkills loads skill definitions from configs/skills and registers each into
|
||||||
|
// the capability registry as a KindSkill capability so the plugin ecosystem can
|
||||||
|
// enumerate them. Load+register is the M5 deliverable; Runner.Run (executing a
|
||||||
|
// skill end to end) lands in a later milestone and currently returns ErrRunPending.
|
||||||
|
func (a *app) wireSkills() {
|
||||||
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||||
|
list, err := skills.NewLoader(a.log).LoadDir(defaultSkillsDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("skills load skipped", "dir", defaultSkillsDir, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
log.Debug("no skills defined", "dir", defaultSkillsDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registered := 0
|
||||||
|
for _, s := range list {
|
||||||
|
if err := skills.Register(a.caps, s); err != nil {
|
||||||
|
log.Warn("skill register skipped", "skill", s.Name(), "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
registered++
|
||||||
|
}
|
||||||
|
log.Info("skills registered", "registered", registered, "loaded", len(list))
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// terminalToolName is the registered name of the exec/terminal tool.
|
||||||
|
const terminalToolName = "terminal"
|
||||||
|
|
||||||
|
// terminalTool is an sdk.Tool that runs a command through the grant-based exec
|
||||||
|
// sandbox (exec.Provider). It is registered even when the sandbox is closed:
|
||||||
|
// Execute then returns exec.ErrExecutionDisabled, so the agent gains the ability
|
||||||
|
// to run code only after the owner explicitly enables the sandbox (default off).
|
||||||
|
type terminalTool struct {
|
||||||
|
prov exec.Provider
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the tool identifier.
|
||||||
|
func (t *terminalTool) Name() string { return terminalToolName }
|
||||||
|
|
||||||
|
// Description explains what the tool does.
|
||||||
|
func (t *terminalTool) Description() string {
|
||||||
|
return "Run a terminal command through the sandboxed exec provider and return its stdout, stderr and exit code."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters returns the input JSON Schema.
|
||||||
|
func (t *terminalTool) Parameters() (map[string]any, error) {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"command": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The command line to run; split into argv by whitespace (no shell interpretation).",
|
||||||
|
},
|
||||||
|
"workdir": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional working directory, confined to an allowed sandbox root.",
|
||||||
|
},
|
||||||
|
"timeout": map[string]any{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Optional timeout in seconds (default 30).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []any{"command"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the command via the exec provider and returns its outcome. When
|
||||||
|
// the sandbox is closed the provider returns exec.ErrExecutionDisabled, which the
|
||||||
|
// dispatcher wraps into a tool error safe to feed back to the LLM.
|
||||||
|
func (t *terminalTool) Execute(ctx context.Context, args any) (any, error) {
|
||||||
|
m, ok := args.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("terminal: args must be an object")
|
||||||
|
}
|
||||||
|
cmd, _ := m["command"].(string)
|
||||||
|
argv := strings.Fields(cmd)
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return nil, fmt.Errorf("terminal: command is required")
|
||||||
|
}
|
||||||
|
if t.log != nil {
|
||||||
|
t.log.Debug("terminal tool invoked", "cmd", strings.Join(argv, " "))
|
||||||
|
}
|
||||||
|
req := exec.ExecRequest{Command: argv, Timeout: timeoutArg(m["timeout"])}
|
||||||
|
if wd, ok := m["workdir"].(string); ok {
|
||||||
|
req.WorkDir = wd
|
||||||
|
}
|
||||||
|
res, err := t.prov.Execute(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"stdout": res.Stdout,
|
||||||
|
"stderr": res.Stderr,
|
||||||
|
"exit": res.ExitCode,
|
||||||
|
"truncated": res.Truncated,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeoutArg converts the optional timeout argument (seconds) to a duration. A
|
||||||
|
// missing or non-positive value returns zero, letting the provider apply its own
|
||||||
|
// default.
|
||||||
|
func timeoutArg(v any) time.Duration {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case float64:
|
||||||
|
if n > 0 {
|
||||||
|
return time.Duration(n * float64(time.Second))
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if n > 0 {
|
||||||
|
return time.Duration(n) * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// wireTerminalTool registers the terminal tool with the dispatcher so the agent
|
||||||
|
// can issue sandboxed terminal commands. It no-ops (with a warning) when no exec
|
||||||
|
// provider is wired, which keeps callers safe.
|
||||||
|
func (a *app) wireTerminalTool() {
|
||||||
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||||
|
if a.execProv == nil {
|
||||||
|
log.Warn("terminal tool skipped: no exec provider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.dispatcher.Register(&terminalTool{
|
||||||
|
prov: a.execProv,
|
||||||
|
log: logger.WithSource(a.log, "cmd/cyrene"),
|
||||||
|
}); err != nil {
|
||||||
|
log.Warn("terminal tool register skipped", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("terminal tool registered", "sandbox_enabled", a.cfg.ExecSandbox.Enabled)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/vision"
|
||||||
|
)
|
||||||
|
|
||||||
|
// visionToolName and ocrToolName are the registered names of the vision and OCR
|
||||||
|
// tools. They are bound to the tool registration here and advertised to the
|
||||||
|
// agent by the tool dispatcher.
|
||||||
|
const (
|
||||||
|
visionToolName = "vision"
|
||||||
|
ocrToolName = "ocr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wireVision registers the vision and OCR tools and the vision model routing.
|
||||||
|
//
|
||||||
|
// The vision adapter is an OpenAI-compatible /chat/completions endpoint on the
|
||||||
|
// same base URL as the chat LLM (LLM_BASE_URL), using the dedicated model
|
||||||
|
// (LLM_VISION_MODEL) when set. When no base URL is configured the tools are
|
||||||
|
// skipped with a warning so startup never blocks; the model router is still
|
||||||
|
// bound to the vision model (in wireRuntime) whenever one is configured.
|
||||||
|
func (a *app) wireVision() {
|
||||||
|
log := logger.WithSource(a.log, "cmd/cyrene")
|
||||||
|
if strings.TrimSpace(a.cfg.LLM.BaseURL) == "" {
|
||||||
|
log.Warn("vision/ocr tools skipped: no LLM base url configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adapter := vision.NewOpenAICompatible(a.cfg.LLM,
|
||||||
|
vision.WithModel(a.cfg.LLM.VisionModel),
|
||||||
|
vision.WithLogger(a.log))
|
||||||
|
if err := vision.RegisterTools(a.caps,
|
||||||
|
vision.NewVisionTool(visionToolName, adapter),
|
||||||
|
vision.NewOCRTool(ocrToolName, adapter),
|
||||||
|
); err != nil {
|
||||||
|
log.Warn("vision/ocr tool register skipped", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("vision/ocr tools registered", "vision_model", a.cfg.LLM.VisionModel)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wireVoice builds the voice STT/TTS degrading chain from config and stores it
|
||||||
|
// on the app for the WebSocket voice_input/voice_output paths to consume. The
|
||||||
|
// chain degrades cloud → local → edge-tts/espeak → silent, so a missing endpoint
|
||||||
|
// or API key never blocks startup (the design accepts the silent fallback).
|
||||||
|
func (a *app) wireVoice() {
|
||||||
|
a.voice = voice.New(a.cfg.Voice)
|
||||||
|
logger.WithSource(a.log, "cmd/cyrene").Info("voice chain ready",
|
||||||
|
"tts_provider", a.cfg.Voice.TTSProvider,
|
||||||
|
"stt_provider", a.cfg.Voice.STTProvider,
|
||||||
|
"dashscope_configured", a.cfg.Voice.DashscopeAPIKey != "")
|
||||||
|
}
|
||||||
+17
-1
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/mcp"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||||
@@ -30,6 +31,7 @@ import (
|
|||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/vector"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/vector"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit"
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/persist"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/persist"
|
||||||
@@ -79,7 +81,10 @@ type app struct {
|
|||||||
loop *loop.Loop
|
loop *loop.Loop
|
||||||
scheduler *orchestration.Scheduler
|
scheduler *orchestration.Scheduler
|
||||||
tasks *task.Manager
|
tasks *task.Manager
|
||||||
|
execProv exec.Provider
|
||||||
iot *iot.Manager
|
iot *iot.Manager
|
||||||
|
voice *voice.Chain
|
||||||
|
mcpRegs []*mcp.Registry
|
||||||
sessions *session.SessionManager
|
sessions *session.SessionManager
|
||||||
automation *automations.Service
|
automation *automations.Service
|
||||||
backthink *backthink.Backthink
|
backthink *backthink.Backthink
|
||||||
@@ -251,7 +256,8 @@ func (a *app) wireRuntime() error {
|
|||||||
a.log.Warn("llm adapter unavailable", "error", aerr)
|
a.log.Warn("llm adapter unavailable", "error", aerr)
|
||||||
adapter = nil
|
adapter = nil
|
||||||
}
|
}
|
||||||
router := model.NewRouter(model.DefaultModelForProvider(a.cfg.LLM))
|
router := model.NewRouter(model.DefaultModelForProvider(a.cfg.LLM),
|
||||||
|
model.WithVisionModel(a.cfg.LLM.VisionModel))
|
||||||
|
|
||||||
dispatcher, derr := tool.New(a.caps, tool.WithDefaultTimeout(defaultToolTimeout))
|
dispatcher, derr := tool.New(a.caps, tool.WithDefaultTimeout(defaultToolTimeout))
|
||||||
if derr != nil {
|
if derr != nil {
|
||||||
@@ -271,6 +277,7 @@ func (a *app) wireRuntime() error {
|
|||||||
a.log.Warn("io tool register skipped", "tool", t.Name(), "error", rerr)
|
a.log.Warn("io tool register skipped", "tool", t.Name(), "error", rerr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
a.wireVision()
|
||||||
|
|
||||||
guards := guardrail.New()
|
guards := guardrail.New()
|
||||||
instructions := a.defaultInstructions()
|
instructions := a.defaultInstructions()
|
||||||
@@ -326,6 +333,7 @@ func (a *app) wireAuxiliary() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("exec provider: %w", err)
|
return fmt.Errorf("exec provider: %w", err)
|
||||||
}
|
}
|
||||||
|
a.execProv = provider
|
||||||
tmgr, err := task.New(a.cfg, provider, a.log, task.WithBus(a.bus))
|
tmgr, err := task.New(a.cfg, provider, a.log, task.WithBus(a.bus))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("task manager: %w", err)
|
return fmt.Errorf("task manager: %w", err)
|
||||||
@@ -365,6 +373,11 @@ func (a *app) wireAuxiliary() error {
|
|||||||
return fmt.Errorf("backthink: %w", err)
|
return fmt.Errorf("backthink: %w", err)
|
||||||
}
|
}
|
||||||
a.backthink = bt
|
a.backthink = bt
|
||||||
|
|
||||||
|
a.wireVoice()
|
||||||
|
a.wireMCP()
|
||||||
|
a.wireSkills()
|
||||||
|
a.wireTerminalTool()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +486,9 @@ func (a *app) close(ctx context.Context) {
|
|||||||
if a.gw != nil {
|
if a.gw != nil {
|
||||||
a.gw.Close()
|
a.gw.Close()
|
||||||
}
|
}
|
||||||
|
for _, reg := range a.mcpRegs {
|
||||||
|
_ = reg.Unregister(ctx)
|
||||||
|
}
|
||||||
if a.tasks != nil {
|
if a.tasks != nil {
|
||||||
_ = a.tasks.Close()
|
_ = a.tasks.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Config struct {
|
|||||||
Background BackgroundConfig
|
Background BackgroundConfig
|
||||||
WSHub WSHubConfig
|
WSHub WSHubConfig
|
||||||
ExecSandbox ExecSandbox
|
ExecSandbox ExecSandbox
|
||||||
|
MCP MCPConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig holds the single gateway listener settings.
|
// ServerConfig holds the single gateway listener settings.
|
||||||
@@ -69,6 +70,10 @@ type LLMConfig struct {
|
|||||||
Model string `env:"LLM_MODEL"`
|
Model string `env:"LLM_MODEL"`
|
||||||
Fallback string `env:"LLM_FALLBACK_MODEL"`
|
Fallback string `env:"LLM_FALLBACK_MODEL"`
|
||||||
|
|
||||||
|
// VisionModel is the dedicated model used for image/vision requests. When
|
||||||
|
// set, the model router routes image content to it (see model.Router.WithVisionModel).
|
||||||
|
VisionModel string `env:"LLM_VISION_MODEL"`
|
||||||
|
|
||||||
// Anthropic native provider, used when Provider=anthropic. Independent
|
// Anthropic native provider, used when Provider=anthropic. Independent
|
||||||
// config block so each vendor is a self-contained config.
|
// config block so each vendor is a self-contained config.
|
||||||
AnthropicBaseURL string `env:"ANTHROPIC_BASE_URL"`
|
AnthropicBaseURL string `env:"ANTHROPIC_BASE_URL"`
|
||||||
@@ -110,6 +115,24 @@ type WSHubConfig struct {
|
|||||||
SessionIdleTimeout int `env:"SESSION_IDLE_TIMEOUT_MIN" default:"30"`
|
SessionIdleTimeout int `env:"SESSION_IDLE_TIMEOUT_MIN" default:"30"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCPConfig holds Model Context Protocol server settings. The reflection-based
|
||||||
|
// loader only supports scalars and []string, so servers are supplied as a JSON
|
||||||
|
// array string (ServersJSON, env MCP_SERVERS_JSON) and parsed at startup by the
|
||||||
|
// MCP wiring (cmd/cyrene/mcp.go).
|
||||||
|
type MCPConfig struct {
|
||||||
|
ServersJSON string `env:"MCP_SERVERS_JSON"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCPServer describes a single MCP server: a stdio subprocess (Command/Args) or
|
||||||
|
// an SSE endpoint (URL). Enabled gates activation at startup.
|
||||||
|
type MCPServer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
// Load reads configuration, merging an optional .env file (development only)
|
// Load reads configuration, merging an optional .env file (development only)
|
||||||
// with process environment variables. Process environment variables always take
|
// with process environment variables. Process environment variables always take
|
||||||
// precedence over .env values, which take precedence over code defaults.
|
// precedence over .env values, which take precedence over code defaults.
|
||||||
|
|||||||
Reference in new issue
Block a user