feat: add workspace-isolated toolhost runtime and capability-gap skill loop

This commit is contained in:
2026-02-28 17:48:33 +08:00
parent ce9346e350
commit 7d6cf6b435
28 changed files with 2223 additions and 143 deletions

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
@@ -13,9 +14,9 @@ import (
"laodingbot/internal/llm"
"laodingbot/internal/logger"
"laodingbot/internal/memory"
"laodingbot/internal/runtimews"
"laodingbot/internal/toolhost"
"laodingbot/internal/tools"
"laodingbot/internal/tools/filetool"
"laodingbot/internal/tools/shelltool"
"laodingbot/internal/transport/feishu"
"laodingbot/internal/transport/telegram"
)
@@ -23,18 +24,29 @@ import (
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
isToolhostChild := len(os.Args) > 1 && os.Args[1] == "--toolhost"
workspaceRoot, err := runtimews.PrepareFromEnv()
if err != nil {
panic(fmt.Sprintf("prepare runtime workspace failed: %v", err))
}
cfg, err := config.Load()
if err != nil {
panic(fmt.Sprintf("load config failed: %v", err))
}
if isToolhostChild {
if err := toolhost.RunChild(ctx, cfg, nil); err != nil && ctx.Err() == nil {
panic(fmt.Sprintf("toolhost child failed: %v", err))
}
return
}
appLogger, err := logger.New(cfg.LogLevel)
if err != nil {
panic(fmt.Sprintf("init logger failed: %v", err))
}
appLogger = appLogger.WithComponent("main")
appLogger.Infof("config loaded; channel=%s, log_level=%s", cfg.MessageChannel, cfg.LogLevel)
appLogger.Infof("config loaded; channel=%s, log_level=%s workspace=%s", cfg.MessageChannel, cfg.LogLevel, workspaceRoot)
store, err := memory.NewSQLiteStore(cfg.SQLitePath, appLogger.WithComponent("memory"))
if err != nil {
@@ -44,19 +56,44 @@ func main() {
defer store.Close()
toolRegistry := tools.NewRegistry(appLogger.WithComponent("tools.registry"))
toolRegistry.Register(filetool.New(cfg.Security.AllowedDirs, appLogger.WithComponent("tools.file")))
toolRegistry.Register(shelltool.New(cfg.Security.AllowedCommands, cfg.Security.WorkDir, 15*time.Second, appLogger.WithComponent("tools.shell")))
exePath, err := os.Executable()
if err != nil {
appLogger.Errorf("resolve executable path failed: %v", err)
panic(err)
}
tc, err := toolhost.NewClient(toolhost.ClientConfig{
ExecutablePath: exePath,
Args: []string{"--toolhost"},
WorkDir: ".",
CallTimeout: time.Duration(cfg.ToolCallTimeoutSec) * time.Second,
HeartbeatInterval: 5 * time.Second,
MaxConcurrency: 4,
}, appLogger.WithComponent("toolhost.client"))
if err != nil {
appLogger.Errorf("init toolhost client failed: %v", err)
panic(err)
}
defer tc.Close()
listCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
toolInfos, err := tc.ToolList(listCtx)
cancel()
if err != nil {
appLogger.Errorf("toolhost list failed: %v", err)
panic(err)
}
if len(toolInfos) == 0 {
panic("toolhost returned empty tool list")
}
for _, info := range toolInfos {
toolRegistry.Register(toolhost.NewRemoteTool(info.Name, info.Description, time.Duration(cfg.ToolCallTimeoutSec)*time.Second, tc))
}
soul, err := knowledge.LoadSoul(cfg.SoulPath)
if err != nil {
appLogger.Errorf("load soul failed path=%s err=%v", cfg.SoulPath, err)
panic(err)
}
skillsDoc, err := knowledge.LoadSkills(cfg.SkillsDir)
if err != nil {
appLogger.Errorf("load skills failed dir=%s err=%v", cfg.SkillsDir, err)
panic(err)
}
skillSet, err := knowledge.LoadSkillSet(cfg.SkillsDir)
if err != nil {
appLogger.Errorf("load skill set failed dir=%s err=%v", cfg.SkillsDir, err)
@@ -71,8 +108,12 @@ func main() {
toolRegistry,
soul,
skillSet,
skillsDoc,
cfg.SkillsDir,
cfg.ReactMaxSteps,
cfg.EnableCapabilityGap,
cfg.AutoSkillDir,
cfg.GapDraftTriggerCount,
time.Duration(cfg.GapClusterLookbackHours)*time.Hour,
appLogger.WithComponent("agent"),
)