refactor(pkg): adapt agent, runtime, migrate, fantasy for SDK/CLI structure
Supporting changes for the extracted CLI and dragonscale SDK: - pkg/agent: agent_run, loop, toolloop adjustments for service wiring - pkg/runtime: bootstrap, config updates for new invocation paths - pkg/migrate: migrate logic updates - pkg/fantasy: adapter changes for tool wiring - pkg/channels: manager tweak - pkg/heartbeat: service adjustment - pkg/messages: types update - pkg/sync: identity_test update Remove t.Parallel from TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid (t.Setenv incompatible with t.Parallel).
This commit is contained in:
parent
3286555321
commit
a76db05a09
15 changed files with 283 additions and 130 deletions
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/constants"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/constants"
|
||||||
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/memory"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/memory"
|
||||||
|
|
@ -93,7 +93,7 @@ func (al *AgentLoop) assembleContext(ctx context.Context, opts processOptions) (
|
||||||
"history": formatMessagesForLog(historyMsgs),
|
"history": formatMessagesForLog(historyMsgs),
|
||||||
})
|
})
|
||||||
|
|
||||||
fantasyHistory := picofantasy.MessagesToFantasy(historyMsgs)
|
fantasyHistory := dragonfantasy.MessagesToFantasy(historyMsgs)
|
||||||
adaptedTools, prepareStep := al.prepareToolset(ctx, opts)
|
adaptedTools, prepareStep := al.prepareToolset(ctx, opts)
|
||||||
agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep)
|
agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -177,13 +177,13 @@ func (al *AgentLoop) splitMessages(opts processOptions, builtMsgs []messages.Mes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([]fantasy.AgentTool, func(context.Context, fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error)) {
|
func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([]fantasy.AgentTool, func(context.Context, fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error)) {
|
||||||
adaptCfg := picofantasy.AdaptedToolsConfig{
|
adaptCfg := dragonfantasy.AdaptedToolsConfig{
|
||||||
MemStore: al.memoryStore,
|
MemStore: al.memoryStore,
|
||||||
AgentID: pkg.NAME,
|
AgentID: pkg.NAME,
|
||||||
SessionKey: opts.SessionKey,
|
SessionKey: opts.SessionKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
adaptedTools := picofantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg)
|
adaptedTools := dragonfantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg)
|
||||||
if al.toolResultSearch != nil {
|
if al.toolResultSearch != nil {
|
||||||
adaptedTools = append(adaptedTools, al.toolResultSearch)
|
adaptedTools = append(adaptedTools, al.toolResultSearch)
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +217,7 @@ func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([
|
||||||
return ctx, fantasy.PrepareStepResult{}, nil
|
return ctx, fantasy.PrepareStepResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newAdapted := picofantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg)
|
newAdapted := dragonfantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg)
|
||||||
expanded := append(adaptedTools, newAdapted...)
|
expanded := append(adaptedTools, newAdapted...)
|
||||||
adaptedTools = expanded
|
adaptedTools = expanded
|
||||||
|
|
||||||
|
|
@ -419,7 +419,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, opts processOptions) (str
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range result.Steps {
|
for _, step := range result.Steps {
|
||||||
stepMsgs := picofantasy.StepToMessages(step)
|
stepMsgs := dragonfantasy.StepToMessages(step)
|
||||||
for _, m := range stepMsgs {
|
for _, m := range stepMsgs {
|
||||||
al.sessions.AddFullMessage(opts.SessionKey, m)
|
al.sessions.AddFullMessage(opts.SessionKey, m)
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +458,7 @@ func (al *AgentLoop) runStreaming(ctx context.Context, opts processOptions, ac a
|
||||||
},
|
},
|
||||||
|
|
||||||
OnStepFinish: func(step fantasy.StepResult) error {
|
OnStepFinish: func(step fantasy.StepResult) error {
|
||||||
stepMsgs := picofantasy.StepToMessages(step)
|
stepMsgs := dragonfantasy.StepToMessages(step)
|
||||||
for _, m := range stepMsgs {
|
for _, m := range stepMsgs {
|
||||||
al.sessions.AddFullMessage(opts.SessionKey, m)
|
al.sessions.AddFullMessage(opts.SessionKey, m)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// DragonScale - Ultra-lightweight personal AI agent
|
// DragonScale - Ultra-lightweight personal AI agent
|
||||||
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
|
||||||
// License: MIT
|
// License: MIT
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 DragonScale contributors
|
// Copyright (c) 2026 DragonScale contributors
|
||||||
|
|
@ -32,7 +32,7 @@ import (
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/session"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/session"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/state"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/state"
|
||||||
picosync "github.com/ZanzyTHEbar/dragonscale/pkg/sync"
|
dragonsync "github.com/ZanzyTHEbar/dragonscale/pkg/sync"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ type AgentLoop struct {
|
||||||
stateStore *StateStore // Agent run state persistence
|
stateStore *StateStore // Agent run state persistence
|
||||||
conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path
|
conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path
|
||||||
conversationMu sync.Mutex // serializes conversation creation path
|
conversationMu sync.Mutex // serializes conversation creation path
|
||||||
identitySync *picosync.IdentitySync // File→DB sync for identity docs (nil if memory disabled)
|
identitySync *dragonsync.IdentitySync // File→DB sync for identity docs (nil if memory disabled)
|
||||||
activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing
|
activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing
|
||||||
running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only
|
running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only
|
||||||
summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path
|
summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path
|
||||||
|
|
@ -217,13 +217,13 @@ func NewAgentLoop(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity file sync (disk → DB)
|
// Identity file sync (disk → DB)
|
||||||
var idSync *picosync.IdentitySync
|
var idSync *dragonsync.IdentitySync
|
||||||
identityDir, idErr := config.IdentityDir()
|
identityDir, idErr := config.IdentityDir()
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled",
|
logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled",
|
||||||
map[string]interface{}{"error": idErr.Error()})
|
map[string]interface{}{"error": idErr.Error()})
|
||||||
} else {
|
} else {
|
||||||
idSync = picosync.New(identityDir, pkg.NAME, memDelegate)
|
idSync = dragonsync.New(identityDir, pkg.NAME, memDelegate)
|
||||||
if syncErr := idSync.SyncAll(ctx); syncErr != nil {
|
if syncErr := idSync.SyncAll(ctx); syncErr != nil {
|
||||||
logger.WarnCF("agent", "Initial identity sync failed (non-fatal)",
|
logger.WarnCF("agent", "Initial identity sync failed (non-fatal)",
|
||||||
map[string]interface{}{"error": syncErr.Error()})
|
map[string]interface{}{"error": syncErr.Error()})
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
fantasy "charm.land/fantasy"
|
fantasy "charm.land/fantasy"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
||||||
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
|
||||||
memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
|
memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
||||||
|
|
@ -90,12 +90,12 @@ func runToolLoopWithRuntime(
|
||||||
return nil, fmt.Errorf("tool runtime is required")
|
return nil, fmt.Errorf("tool runtime is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
adaptCfg := picofantasy.AdaptedToolsConfig{
|
adaptCfg := dragonfantasy.AdaptedToolsConfig{
|
||||||
MemStore: ms,
|
MemStore: ms,
|
||||||
AgentID: pkg.NAME,
|
AgentID: pkg.NAME,
|
||||||
SessionKey: sessionKey,
|
SessionKey: sessionKey,
|
||||||
}
|
}
|
||||||
adaptedTools := picofantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg)
|
adaptedTools := dragonfantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg)
|
||||||
if len(extraTools) > 0 {
|
if len(extraTools) > 0 {
|
||||||
adaptedTools = append(adaptedTools, extraTools...)
|
adaptedTools = append(adaptedTools, extraTools...)
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +126,7 @@ func runToolLoopWithRuntime(
|
||||||
if len(newTools) == 0 {
|
if len(newTools) == 0 {
|
||||||
return ctx, fantasy.PrepareStepResult{}, nil
|
return ctx, fantasy.PrepareStepResult{}, nil
|
||||||
}
|
}
|
||||||
newAdapted := picofantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg)
|
newAdapted := dragonfantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg)
|
||||||
adaptedTools = append(adaptedTools, newAdapted...)
|
adaptedTools = append(adaptedTools, newAdapted...)
|
||||||
return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil
|
return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// DragonScale - Ultra-lightweight personal AI agent
|
// DragonScale - Ultra-lightweight personal AI agent
|
||||||
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
|
||||||
// License: MIT
|
// License: MIT
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 DragonScale contributors
|
// Copyright (c) 2026 DragonScale contributors
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ import (
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PicoToolAdapter wraps a DragonScale tool as a Fantasy AgentTool.
|
// DragonToolAdapter wraps a DragonScale tool as a Fantasy AgentTool.
|
||||||
// It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's
|
// It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's
|
||||||
// simple ToolResponse by publishing ForUser content to the bus as a side effect
|
// simple ToolResponse by publishing ForUser content to the bus as a side effect
|
||||||
// and returning only ForLLM content to Fantasy.
|
// and returning only ForLLM content to Fantasy.
|
||||||
type PicoToolAdapter struct {
|
type DragonToolAdapter struct {
|
||||||
inner tools.Tool
|
inner tools.Tool
|
||||||
bus *bus.MessageBus
|
bus *bus.MessageBus
|
||||||
channel string
|
channel string
|
||||||
|
|
@ -32,8 +32,8 @@ type PicoToolAdapter struct {
|
||||||
sessionKey string
|
sessionKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check that PicoToolAdapter implements fantasy.AgentTool.
|
// Compile-time check that DragonToolAdapter implements fantasy.AgentTool.
|
||||||
var _ fantasy.AgentTool = (*PicoToolAdapter)(nil)
|
var _ fantasy.AgentTool = (*DragonToolAdapter)(nil)
|
||||||
|
|
||||||
// Info returns Fantasy-compatible tool metadata from the DragonScale tool.
|
// Info returns Fantasy-compatible tool metadata from the DragonScale tool.
|
||||||
// Fantasy's ToolInfo expects Parameters to be just the properties map and
|
// Fantasy's ToolInfo expects Parameters to be just the properties map and
|
||||||
|
|
@ -41,7 +41,7 @@ var _ fantasy.AgentTool = (*PicoToolAdapter)(nil)
|
||||||
// Schema object from Parameters() (with "type", "properties", "required"
|
// Schema object from Parameters() (with "type", "properties", "required"
|
||||||
// keys), so we must unwrap it here to avoid double-wrapping in
|
// keys), so we must unwrap it here to avoid double-wrapping in
|
||||||
// agent.prepareTools() and agent.validateToolCall().
|
// agent.prepareTools() and agent.validateToolCall().
|
||||||
func (a *PicoToolAdapter) Info() fantasy.ToolInfo {
|
func (a *DragonToolAdapter) Info() fantasy.ToolInfo {
|
||||||
params := a.inner.Parameters()
|
params := a.inner.Parameters()
|
||||||
properties, required := unwrapSchema(params)
|
properties, required := unwrapSchema(params)
|
||||||
return fantasy.ToolInfo{
|
return fantasy.ToolInfo{
|
||||||
|
|
@ -83,7 +83,7 @@ func unwrapSchema(params map[string]interface{}) (map[string]interface{}, []stri
|
||||||
// - If the tool result has ForUser content and is not Silent, publishes to the bus.
|
// - If the tool result has ForUser content and is not Silent, publishes to the bus.
|
||||||
// - If the tool is a ContextualTool, sets channel/chatID context before execution.
|
// - If the tool is a ContextualTool, sets channel/chatID context before execution.
|
||||||
// - If the tool is an AsyncTool, wires a callback that publishes results to the bus.
|
// - If the tool is an AsyncTool, wires a callback that publishes results to the bus.
|
||||||
func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
func (a *DragonToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
||||||
// 1. Deserialize Fantasy's JSON string input into DragonScale's map format.
|
// 1. Deserialize Fantasy's JSON string input into DragonScale's map format.
|
||||||
args, err := parseToolArgs(call.Input)
|
args, err := parseToolArgs(call.Input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -147,12 +147,12 @@ func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fanta
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderOptions returns nil — DragonScale tools have no provider-specific options.
|
// ProviderOptions returns nil — DragonScale tools have no provider-specific options.
|
||||||
func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions {
|
func (a *DragonToolAdapter) ProviderOptions() fantasy.ProviderOptions {
|
||||||
return fantasy.ProviderOptions{}
|
return fantasy.ProviderOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProviderOptions is a no-op for DragonScale tools.
|
// SetProviderOptions is a no-op for DragonScale tools.
|
||||||
func (a *PicoToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {}
|
func (a *DragonToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {}
|
||||||
|
|
||||||
// AdaptedToolsConfig configures how tools are adapted for the Fantasy agent.
|
// AdaptedToolsConfig configures how tools are adapted for the Fantasy agent.
|
||||||
type AdaptedToolsConfig struct {
|
type AdaptedToolsConfig struct {
|
||||||
|
|
@ -182,7 +182,7 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
adapted = append(adapted, &PicoToolAdapter{
|
adapted = append(adapted, &DragonToolAdapter{
|
||||||
inner: tool,
|
inner: tool,
|
||||||
bus: msgBus,
|
bus: msgBus,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
|
@ -198,10 +198,10 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
|
||||||
|
|
||||||
// AdaptTools wraps specific Tool instances as Fantasy AgentTools.
|
// AdaptTools wraps specific Tool instances as Fantasy AgentTools.
|
||||||
// Used by PrepareStep to promote discovered tools to native callables.
|
// Used by PrepareStep to promote discovered tools to native callables.
|
||||||
func AdaptTools(picoTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool {
|
func AdaptTools(dragonTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool {
|
||||||
adapted := make([]fantasy.AgentTool, 0, len(picoTools))
|
adapted := make([]fantasy.AgentTool, 0, len(dragonTools))
|
||||||
for _, tool := range picoTools {
|
for _, tool := range dragonTools {
|
||||||
adapted = append(adapted, &PicoToolAdapter{
|
adapted = append(adapted, &DragonToolAdapter{
|
||||||
inner: tool,
|
inner: tool,
|
||||||
bus: msgBus,
|
bus: msgBus,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
|
|
||||||
|
|
@ -100,11 +100,11 @@ func (t *mockNilResultTool) Execute(_ context.Context, _ map[string]interface{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PicoToolAdapter.Info() Tests ---
|
// --- DragonToolAdapter.Info() Tests ---
|
||||||
|
|
||||||
func TestAdapter_Info(t *testing.T) {
|
func TestAdapter_Info(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
adapter := &PicoToolAdapter{inner: &mockDualChannelTool{}}
|
adapter := &DragonToolAdapter{inner: &mockDualChannelTool{}}
|
||||||
info := adapter.Info()
|
info := adapter.Info()
|
||||||
|
|
||||||
if info.Name != "dual_tool" {
|
if info.Name != "dual_tool" {
|
||||||
|
|
@ -130,7 +130,7 @@ func TestAdapter_Info(t *testing.T) {
|
||||||
func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) {
|
func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
mock := &mockToolWithRequired{}
|
mock := &mockToolWithRequired{}
|
||||||
adapter := &PicoToolAdapter{inner: mock}
|
adapter := &DragonToolAdapter{inner: mock}
|
||||||
info := adapter.Info()
|
info := adapter.Info()
|
||||||
|
|
||||||
if _, ok := info.Parameters["path"]; !ok {
|
if _, ok := info.Parameters["path"]; !ok {
|
||||||
|
|
@ -158,12 +158,12 @@ func (t *mockToolWithRequired) Execute(_ context.Context, _ map[string]interface
|
||||||
return &tools.ToolResult{ForLLM: "ok"}
|
return &tools.ToolResult{ForLLM: "ok"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PicoToolAdapter.Run() Tests ---
|
// --- DragonToolAdapter.Run() Tests ---
|
||||||
|
|
||||||
func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: &mockSilentTool{},
|
inner: &mockSilentTool{},
|
||||||
bus: msgBus,
|
bus: msgBus,
|
||||||
channel: "test",
|
channel: "test",
|
||||||
|
|
@ -192,7 +192,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
||||||
func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: &mockDualChannelTool{},
|
inner: &mockDualChannelTool{},
|
||||||
bus: msgBus,
|
bus: msgBus,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
|
|
@ -221,7 +221,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
||||||
|
|
||||||
func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: &mockErrorTool{},
|
inner: &mockErrorTool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +246,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
||||||
|
|
||||||
func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: &mockNilResultTool{},
|
inner: &mockNilResultTool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +272,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
||||||
func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctxTool := &mockContextualTool{}
|
ctxTool := &mockContextualTool{}
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: ctxTool,
|
inner: ctxTool,
|
||||||
channel: "discord",
|
channel: "discord",
|
||||||
chatID: "guild-1",
|
chatID: "guild-1",
|
||||||
|
|
@ -297,7 +297,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
||||||
|
|
||||||
func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
adapter := &PicoToolAdapter{
|
adapter := &DragonToolAdapter{
|
||||||
inner: &mockSilentTool{},
|
inner: &mockSilentTool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -428,7 +428,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
|
||||||
|
|
||||||
// --- Round-trip fidelity test ---
|
// --- Round-trip fidelity test ---
|
||||||
|
|
||||||
func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) {
|
func TestRoundTrip_DragonScaleToFantasyAndBack(t *testing.T) {
|
||||||
t.Parallel(
|
t.Parallel(
|
||||||
// Start with DragonScale messages representing a typical conversation
|
// Start with DragonScale messages representing a typical conversation
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// DragonScale - Ultra-lightweight personal AI agent
|
// DragonScale - Ultra-lightweight personal AI agent
|
||||||
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
|
||||||
// License: MIT
|
// License: MIT
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 DragonScale contributors
|
// Copyright (c) 2026 DragonScale contributors
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// DragonScale - Ultra-lightweight personal AI agent
|
// DragonScale - Ultra-lightweight personal AI agent
|
||||||
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
|
||||||
// License: MIT
|
// License: MIT
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 DragonScale contributors
|
// Copyright (c) 2026 DragonScale contributors
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ type Options struct {
|
||||||
Force bool
|
Force bool
|
||||||
Refresh bool
|
Refresh bool
|
||||||
OpenClawHome string
|
OpenClawHome string
|
||||||
PicoClawHome string
|
DragonScaleHome string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action struct {
|
type Action struct {
|
||||||
|
|
@ -62,7 +62,7 @@ func Run(opts Options) (*Result, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
picoClawHome, err := resolvePicoClawHome(opts.PicoClawHome)
|
dragonscaleHome, err := resolveDragonScaleHome(opts.DragonScaleHome)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -71,14 +71,14 @@ func Run(opts Options) (*Result, error) {
|
||||||
return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome)
|
return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
actions, warnings, err := Plan(opts, openclawHome, picoClawHome)
|
actions, warnings, err := Plan(opts, openclawHome, dragonscaleHome)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Migrating from OpenClaw to DragonScale")
|
fmt.Println("Migrating from OpenClaw to DragonScale")
|
||||||
fmt.Printf(" Source: %s\n", openclawHome)
|
fmt.Printf(" Source: %s\n", openclawHome)
|
||||||
fmt.Printf(" Destination: %s\n", picoClawHome)
|
fmt.Printf(" Destination: %s\n", dragonscaleHome)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
|
|
@ -95,12 +95,12 @@ func Run(opts Options) (*Result, error) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
result := Execute(actions, openclawHome, picoClawHome)
|
result := Execute(actions, openclawHome, dragonscaleHome)
|
||||||
result.Warnings = warnings
|
result.Warnings = warnings
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, error) {
|
func Plan(opts Options, openclawHome, dragonscaleHome string) ([]Action, []string, error) {
|
||||||
var actions []Action
|
var actions []Action
|
||||||
var warnings []string
|
var warnings []string
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
||||||
actions = append(actions, Action{
|
actions = append(actions, Action{
|
||||||
Type: ActionConvertConfig,
|
Type: ActionConvertConfig,
|
||||||
Source: configPath,
|
Source: configPath,
|
||||||
Destination: filepath.Join(picoClawHome, "config.json"),
|
Destination: filepath.Join(dragonscaleHome, "config.json"),
|
||||||
Description: "convert OpenClaw config to DragonScale format",
|
Description: "convert OpenClaw config to DragonScale format",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
||||||
|
|
||||||
if !opts.ConfigOnly {
|
if !opts.ConfigOnly {
|
||||||
srcWorkspace := resolveWorkspace(openclawHome)
|
srcWorkspace := resolveWorkspace(openclawHome)
|
||||||
dstWorkspace := resolveWorkspace(picoClawHome)
|
dstWorkspace := resolveWorkspace(dragonscaleHome)
|
||||||
|
|
||||||
if _, err := os.Stat(srcWorkspace); err == nil {
|
if _, err := os.Stat(srcWorkspace); err == nil {
|
||||||
wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force)
|
wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force)
|
||||||
|
|
@ -147,13 +147,13 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
||||||
return actions, warnings, nil
|
return actions, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
func Execute(actions []Action, openclawHome, dragonscaleHome string) *Result {
|
||||||
result := &Result{}
|
result := &Result{}
|
||||||
|
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
switch action.Type {
|
switch action.Type {
|
||||||
case ActionConvertConfig:
|
case ActionConvertConfig:
|
||||||
if err := executeConfigMigration(action.Source, action.Destination, picoClawHome); err != nil {
|
if err := executeConfigMigration(action.Source, action.Destination, dragonscaleHome); err != nil {
|
||||||
result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err))
|
result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err))
|
||||||
fmt.Printf(" ✗ Config migration failed: %v\n", err)
|
fmt.Printf(" ✗ Config migration failed: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,7 +207,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) error {
|
func executeConfigMigration(srcConfigPath, dstConfigPath, dragonscaleHome string) error {
|
||||||
data, err := LoadOpenClawConfig(srcConfigPath)
|
data, err := LoadOpenClawConfig(srcConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -326,7 +326,7 @@ func resolveOpenClawHome(override string) (string, error) {
|
||||||
return filepath.Join(home, ".openclaw"), nil
|
return filepath.Join(home, ".openclaw"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePicoClawHome(override string) (string, error) {
|
func resolveDragonScaleHome(override string) (string, error) {
|
||||||
if override != "" {
|
if override != "" {
|
||||||
return expandHome(override), nil
|
return expandHome(override), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -579,7 +579,7 @@ func TestRewriteWorkspacePath(t *testing.T) {
|
||||||
func TestRunDryRun(t *testing.T) {
|
func TestRunDryRun(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
openclawHome := t.TempDir()
|
openclawHome := t.TempDir()
|
||||||
picoClawHome := t.TempDir()
|
dragonscaleHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0755)
|
||||||
|
|
@ -599,7 +599,7 @@ func TestRunDryRun(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
OpenClawHome: openclawHome,
|
OpenClawHome: openclawHome,
|
||||||
PicoClawHome: picoClawHome,
|
DragonScaleHome: dragonscaleHome,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := Run(opts)
|
result, err := Run(opts)
|
||||||
|
|
@ -607,11 +607,11 @@ func TestRunDryRun(t *testing.T) {
|
||||||
t.Fatalf("Run: %v", err)
|
t.Fatalf("Run: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||||
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||||
t.Error("dry run should not create files")
|
t.Error("dry run should not create files")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(picoClawHome, "config.json")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(dragonscaleHome, "config.json")); !os.IsNotExist(err) {
|
||||||
t.Error("dry run should not create config")
|
t.Error("dry run should not create config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,7 +621,7 @@ func TestRunDryRun(t *testing.T) {
|
||||||
func TestRunFullMigration(t *testing.T) {
|
func TestRunFullMigration(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
openclawHome := t.TempDir()
|
openclawHome := t.TempDir()
|
||||||
picoClawHome := t.TempDir()
|
dragonscaleHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0755)
|
||||||
|
|
@ -655,7 +655,7 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
OpenClawHome: openclawHome,
|
OpenClawHome: openclawHome,
|
||||||
PicoClawHome: picoClawHome,
|
DragonScaleHome: dragonscaleHome,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := Run(opts)
|
result, err := Run(opts)
|
||||||
|
|
@ -663,9 +663,9 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
t.Fatalf("Run: %v", err)
|
t.Fatalf("Run: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||||
|
|
||||||
soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md"))
|
soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading SOUL.md: %v", err)
|
t.Fatalf("reading SOUL.md: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -673,7 +673,7 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw")
|
t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw")
|
||||||
}
|
}
|
||||||
|
|
||||||
agentsData, err := os.ReadFile(filepath.Join(picoWs, "AGENTS.md"))
|
agentsData, err := os.ReadFile(filepath.Join(dragonWs, "AGENTS.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading AGENTS.md: %v", err)
|
t.Fatalf("reading AGENTS.md: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -681,7 +681,7 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
t.Errorf("AGENTS.md content = %q", string(agentsData))
|
t.Errorf("AGENTS.md content = %q", string(agentsData))
|
||||||
}
|
}
|
||||||
|
|
||||||
memData, err := os.ReadFile(filepath.Join(picoWs, "memory", "MEMORY.md"))
|
memData, err := os.ReadFile(filepath.Join(dragonWs, "memory", "MEMORY.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading memory/MEMORY.md: %v", err)
|
t.Fatalf("reading memory/MEMORY.md: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -689,21 +689,21 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
t.Errorf("MEMORY.md content = %q", string(memData))
|
t.Errorf("MEMORY.md content = %q", string(memData))
|
||||||
}
|
}
|
||||||
|
|
||||||
picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json"))
|
dragonConfig, err := config.LoadConfig(filepath.Join(dragonscaleHome, "config.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("loading DragonScale config: %v", err)
|
t.Fatalf("loading DragonScale config: %v", err)
|
||||||
}
|
}
|
||||||
if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" {
|
if dragonConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" {
|
||||||
t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test")
|
t.Errorf("Anthropic.APIKey = %q, want %q", dragonConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test")
|
||||||
}
|
}
|
||||||
if picoConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" {
|
if dragonConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" {
|
||||||
t.Errorf("OpenRouter.APIKey = %q, want %q", picoConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test")
|
t.Errorf("OpenRouter.APIKey = %q, want %q", dragonConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test")
|
||||||
}
|
}
|
||||||
if !picoConfig.Channels.Telegram.Enabled {
|
if !dragonConfig.Channels.Telegram.Enabled {
|
||||||
t.Error("Telegram should be enabled")
|
t.Error("Telegram should be enabled")
|
||||||
}
|
}
|
||||||
if picoConfig.Channels.Telegram.Token != "tg-migrate-test" {
|
if dragonConfig.Channels.Telegram.Token != "tg-migrate-test" {
|
||||||
t.Errorf("Telegram.Token = %q, want %q", picoConfig.Channels.Telegram.Token, "tg-migrate-test")
|
t.Errorf("Telegram.Token = %q, want %q", dragonConfig.Channels.Telegram.Token, "tg-migrate-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.FilesCopied < 3 {
|
if result.FilesCopied < 3 {
|
||||||
|
|
@ -721,7 +721,7 @@ func TestRunOpenClawNotFound(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
opts := Options{
|
opts := Options{
|
||||||
OpenClawHome: "/nonexistent/path/to/openclaw",
|
OpenClawHome: "/nonexistent/path/to/openclaw",
|
||||||
PicoClawHome: t.TempDir(),
|
DragonScaleHome: t.TempDir(),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := Run(opts)
|
_, err := Run(opts)
|
||||||
|
|
@ -787,7 +787,7 @@ func TestCopyFile(t *testing.T) {
|
||||||
func TestRunConfigOnly(t *testing.T) {
|
func TestRunConfigOnly(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
openclawHome := t.TempDir()
|
openclawHome := t.TempDir()
|
||||||
picoClawHome := t.TempDir()
|
dragonscaleHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0755)
|
||||||
|
|
@ -807,7 +807,7 @@ func TestRunConfigOnly(t *testing.T) {
|
||||||
Force: true,
|
Force: true,
|
||||||
ConfigOnly: true,
|
ConfigOnly: true,
|
||||||
OpenClawHome: openclawHome,
|
OpenClawHome: openclawHome,
|
||||||
PicoClawHome: picoClawHome,
|
DragonScaleHome: dragonscaleHome,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := Run(opts)
|
result, err := Run(opts)
|
||||||
|
|
@ -819,8 +819,8 @@ func TestRunConfigOnly(t *testing.T) {
|
||||||
t.Error("config should have been migrated")
|
t.Error("config should have been migrated")
|
||||||
}
|
}
|
||||||
|
|
||||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||||
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||||
t.Error("config-only should not copy workspace files")
|
t.Error("config-only should not copy workspace files")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -828,7 +828,7 @@ func TestRunConfigOnly(t *testing.T) {
|
||||||
func TestRunWorkspaceOnly(t *testing.T) {
|
func TestRunWorkspaceOnly(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
openclawHome := t.TempDir()
|
openclawHome := t.TempDir()
|
||||||
picoClawHome := t.TempDir()
|
dragonscaleHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0755)
|
||||||
|
|
@ -848,7 +848,7 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
||||||
Force: true,
|
Force: true,
|
||||||
WorkspaceOnly: true,
|
WorkspaceOnly: true,
|
||||||
OpenClawHome: openclawHome,
|
OpenClawHome: openclawHome,
|
||||||
PicoClawHome: picoClawHome,
|
DragonScaleHome: dragonscaleHome,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := Run(opts)
|
result, err := Run(opts)
|
||||||
|
|
@ -860,8 +860,8 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
||||||
t.Error("workspace-only should not migrate config")
|
t.Error("workspace-only should not migrate config")
|
||||||
}
|
}
|
||||||
|
|
||||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||||
soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md"))
|
soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("reading SOUL.md: %v", err)
|
t.Fatalf("reading SOUL.md: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/agent"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/agent"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
||||||
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OutboundMode string
|
type OutboundMode string
|
||||||
|
|
@ -72,13 +72,13 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions
|
||||||
|
|
||||||
ctx, cancel := withExecutionContext(parent, opts.Timeout)
|
ctx, cancel := withExecutionContext(parent, opts.Timeout)
|
||||||
|
|
||||||
provider, err := picofantasy.CreateProvider(cfg)
|
provider, err := dragonfantasy.CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, fmt.Errorf("provider error: %w", err)
|
return nil, fmt.Errorf("provider error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
model, err := provider.LanguageModel(ctx, picofantasy.ModelID(cfg))
|
model, err := provider.LanguageModel(ctx, dragonfantasy.ModelID(cfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, fmt.Errorf("model error: %w", err)
|
return nil, fmt.Errorf("model error: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,15 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
||||||
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG"
|
const (
|
||||||
|
EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG"
|
||||||
|
EvalBaseConfigEnvVar = "DRAGONSCALE_EVAL_BASE_CONFIG"
|
||||||
|
EvalHostHomeEnvVar = "DRAGONSCALE_EVAL_HOST_HOME"
|
||||||
|
)
|
||||||
|
|
||||||
type LoadConfigOptions struct {
|
type LoadConfigOptions struct {
|
||||||
BaseConfigPath string
|
BaseConfigPath string
|
||||||
|
|
@ -18,24 +23,51 @@ type LoadConfigOptions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveBaseConfigPath() string {
|
func ResolveBaseConfigPath() string {
|
||||||
|
if explicit := os.Getenv(EvalBaseConfigEnvVar); explicit != "" {
|
||||||
|
if _, err := os.Stat(explicit); err == nil {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkHostConfig := func(hostHome string) string {
|
||||||
|
if hostHome == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// XDG standard path.
|
||||||
|
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
|
||||||
|
if _, err := os.Stat(hostXDG); err == nil {
|
||||||
|
return hostXDG
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer host-mounted config when running in containerized eval.
|
||||||
|
if hostHome := os.Getenv(EvalHostHomeEnvVar); hostHome != "" {
|
||||||
|
if path := checkHostConfig(hostHome); path != "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the default devcontainer host mount location.
|
||||||
|
if path := checkHostConfig("/host_home"); path != "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer XDG standard path (~/.config/dragonscale/config.json) when present.
|
// Prefer XDG standard path (~/.config/dragonscale/config.json) when present.
|
||||||
if xdgPath, err := config.DefaultConfigPath(); err == nil {
|
xdgPath, err := config.DefaultConfigPath()
|
||||||
|
if err == nil {
|
||||||
if _, statErr := os.Stat(xdgPath); statErr == nil {
|
if _, statErr := os.Stat(xdgPath); statErr == nil {
|
||||||
return xdgPath
|
return xdgPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
legacy := filepath.Join(home, ".dragonscale", "config.json")
|
|
||||||
if _, err := os.Stat(legacy); err == nil {
|
|
||||||
return legacy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neither exists; return XDG path if resolvable so defaults still load.
|
// Neither exists; return XDG path if resolvable so defaults still load.
|
||||||
if xdgPath, err := config.DefaultConfigPath(); err == nil {
|
if err == nil {
|
||||||
return xdgPath
|
return xdgPath
|
||||||
}
|
}
|
||||||
return legacy
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) {
|
func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) {
|
||||||
|
|
|
||||||
|
|
@ -44,18 +44,139 @@ func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) {
|
||||||
assert.Empty(t, cmp.Diff(xdgPath, got))
|
assert.Empty(t, cmp.Diff(xdgPath, got))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) {
|
func TestResolveBaseConfigPath_FallsBackToXDGWhenLegacyOnlyExists(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
xdg := t.TempDir()
|
xdg := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
t.Setenv("XDG_CONFIG_HOME", xdg)
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
|
||||||
|
xdgPath := filepath.Join(xdg, pkg.NAME, "config.json")
|
||||||
legacyPath := filepath.Join(home, ".dragonscale", "config.json")
|
legacyPath := filepath.Join(home, ".dragonscale", "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(xdgPath), 0o755))
|
||||||
require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755))
|
require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(xdgPath, []byte(`{}`), 0o644))
|
||||||
require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644))
|
require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
got := ResolveBaseConfigPath()
|
got := ResolveBaseConfigPath()
|
||||||
assert.Empty(t, cmp.Diff(legacyPath, got))
|
assert.Empty(t, cmp.Diff(xdgPath, got))
|
||||||
|
assert.NotEmpty(t, legacyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_UsesEvalHostHomeWhenContainerConfigMissing(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
hostHome := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostHome)
|
||||||
|
|
||||||
|
hostXDGConfig := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDGConfig), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(hostXDGConfig, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(hostXDGConfig, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_PrefersEvalHostHomeOverContainerConfig(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
hostHome := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostHome)
|
||||||
|
|
||||||
|
containerXDG := filepath.Join(xdg, pkg.NAME, "config.json")
|
||||||
|
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(hostXDG, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_DoesNotUseLegacyHostPathForHostHome(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
hostHome := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostHome)
|
||||||
|
|
||||||
|
containerXDG := filepath.Join(xdg, pkg.NAME, "config.json")
|
||||||
|
hostLegacy := filepath.Join(hostHome, ".dragonscale", "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hostLegacy), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(hostLegacy, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(containerXDG, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_UsesEvalBaseConfigOverride(t *testing.T) {
|
||||||
|
override := filepath.Join(t.TempDir(), "explicit-config.json")
|
||||||
|
require.NoError(t, os.WriteFile(override, []byte(`{}`), 0o644))
|
||||||
|
t.Setenv(EvalBaseConfigEnvVar, override)
|
||||||
|
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(override, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_UsesEvalBaseConfigUnsetAsFallback(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
hostHome := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostHome)
|
||||||
|
|
||||||
|
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
|
t.Setenv(EvalBaseConfigEnvVar, "")
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(hostXDG, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveBaseConfigPath_InvalidEvalBaseConfigFallsBackToHostConfig(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
hostHome := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostHome)
|
||||||
|
|
||||||
|
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
|
||||||
|
|
||||||
|
t.Setenv(EvalBaseConfigEnvVar, filepath.Join(t.TempDir(), "no-such-file.json"))
|
||||||
|
got := ResolveBaseConfigPath()
|
||||||
|
assert.Empty(t, cmp.Diff(hostXDG, got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid(t *testing.T) {
|
||||||
|
workDir := t.TempDir()
|
||||||
|
hostDir := t.TempDir()
|
||||||
|
home := t.TempDir()
|
||||||
|
xdg := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||||
|
t.Setenv(EvalHostHomeEnvVar, hostDir)
|
||||||
|
|
||||||
|
basePath := filepath.Join(hostDir, ".config", pkg.NAME, "config.json")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(basePath), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(basePath, []byte(`{"agents":{"defaults":{"restrict_to_sandbox":true,"max_tool_iterations":20}}}`), 0o644))
|
||||||
|
|
||||||
|
t.Setenv(EvalBaseConfigEnvVar, filepath.Join(workDir, "does-not-exist.json"))
|
||||||
|
|
||||||
|
cfg, err := LoadResolvedConfig(LoadConfigOptions{MinProviderTimeout: 2 * time.Second})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, cmp.Diff(true, cfg.Agents.Defaults.RestrictToSandbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) {
|
func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) {
|
||||||
"AGENT.md": "# Agent\nYou are helpful.",
|
"AGENT.md": "# Agent\nYou are helpful.",
|
||||||
"SOUL.md": "# Soul\nCurious and kind.",
|
"SOUL.md": "# Soul\nCurious and kind.",
|
||||||
"USER.md": "# User\nName: Alice",
|
"USER.md": "# User\nName: Alice",
|
||||||
"IDENTITY.md": "# Identity\nPicoClaw v1",
|
"IDENTITY.md": "# Identity\nDragonScale v1",
|
||||||
})
|
})
|
||||||
|
|
||||||
store := newMockStore()
|
store := newMockStore()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue