- Add DeepSeekClient implementing LLM interface - Support chat and streaming APIs - Add Provider config option (ollama/deepseek) - Default to DeepSeek with model deepseek-v4-flash - Update CLI to display provider info - Add DeepSeek environment variables (DEEPSEEK_API_KEY, etc.)
440 lines
11 KiB
Go
440 lines
11 KiB
Go
// Package kernel implements the microkernel core of the Orca framework.
|
|
//
|
|
// The kernel is the minimal runtime that manages plugin lifecycle,
|
|
// message routing, and inter-component communication.
|
|
package kernel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/orca/orca/internal/config"
|
|
"github.com/orca/orca/pkg/actor"
|
|
"github.com/orca/orca/pkg/bus"
|
|
"github.com/orca/orca/pkg/llm"
|
|
"github.com/orca/orca/pkg/plugin"
|
|
"github.com/orca/orca/pkg/session"
|
|
"github.com/orca/orca/pkg/skill"
|
|
"github.com/orca/orca/pkg/tool"
|
|
)
|
|
|
|
// Kernel is the microkernel core of the Orca framework.
|
|
//
|
|
// It orchestrates plugin lifecycle, message routing, and inter-component
|
|
// communication. The kernel initializes and manages:
|
|
// - Message bus for inter-component communication
|
|
// - Plugin registry for extensibility
|
|
// - Session manager for conversation persistence
|
|
// - Tool manager with built-in tools
|
|
// - Skill manager for skill-based automation
|
|
// - Actor system with orchestrator, workers, and LLM agent
|
|
type Kernel struct {
|
|
mu sync.RWMutex
|
|
mb bus.MessageBus
|
|
registry *plugin.Registry
|
|
plugins []plugin.Plugin
|
|
started bool
|
|
|
|
// Integration components
|
|
config *config.Config
|
|
sessionMgr *session.Manager
|
|
toolMgr *tool.Manager
|
|
skillMgr *skill.Manager
|
|
actorSystem *actor.System
|
|
orch *actor.Orchestrator
|
|
llmAgent *actor.LLMAgent
|
|
toolWorker *actor.ToolWorker
|
|
}
|
|
|
|
// New creates a new Kernel instance with default configuration.
|
|
func New() *Kernel {
|
|
return NewWithConfig(config.DefaultConfig())
|
|
}
|
|
|
|
// NewWithConfig creates a new Kernel instance with the given configuration.
|
|
func NewWithConfig(cfg *config.Config) *Kernel {
|
|
if cfg == nil {
|
|
cfg = config.DefaultConfig()
|
|
}
|
|
|
|
k := &Kernel{
|
|
mb: bus.New(),
|
|
registry: plugin.NewRegistry(),
|
|
config: cfg,
|
|
actorSystem: actor.NewSystem(),
|
|
}
|
|
|
|
// Initialize session manager
|
|
store, err := session.NewJSONLStore(cfg.Session.StorageDir)
|
|
if err != nil {
|
|
log.Printf("kernel: warning: failed to create session store: %v", err)
|
|
} else {
|
|
k.sessionMgr = session.NewManager(store, k.mb)
|
|
}
|
|
|
|
// Initialize tool manager with all built-in tools
|
|
k.toolMgr = tool.NewManager()
|
|
k.registerBuiltinTools()
|
|
|
|
// Initialize skill manager
|
|
k.skillMgr = skill.NewManager(cfg.Session.StorageDir + "/skills")
|
|
|
|
// Initialize actor system
|
|
k.initializeActorSystem()
|
|
|
|
return k
|
|
}
|
|
|
|
// registerBuiltinTools registers all built-in tools with the tool manager.
|
|
func (k *Kernel) registerBuiltinTools() {
|
|
tools := []tool.Tool{
|
|
tool.NewExecTool(nil), // exec - shell commands
|
|
tool.NewReadFileTool(), // read_file
|
|
tool.NewWriteFileTool(), // write_file
|
|
tool.NewListDirTool(), // list_dir
|
|
tool.NewSearchFilesTool(), // search_files
|
|
}
|
|
|
|
for _, t := range tools {
|
|
if err := k.toolMgr.Register(t); err != nil {
|
|
log.Printf("kernel: warning: failed to register tool %q: %v", t.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// initializeActorSystem sets up the orchestrator, tool worker, and LLM agent.
|
|
func (k *Kernel) initializeActorSystem() {
|
|
// Create orchestrator
|
|
orch, err := k.actorSystem.CreateOrchestrator(k)
|
|
if err != nil {
|
|
log.Printf("kernel: warning: failed to create orchestrator: %v", err)
|
|
return
|
|
}
|
|
k.orch = orch
|
|
|
|
// Create tool worker
|
|
tw, err := k.actorSystem.CreateToolWorker(k.toolMgr)
|
|
if err != nil {
|
|
log.Printf("kernel: warning: failed to create tool worker: %v", err)
|
|
return
|
|
}
|
|
k.toolWorker = tw
|
|
|
|
// Create LLM backend
|
|
ollama := k.createLLMBackend()
|
|
|
|
// Create LLM agent
|
|
llmAgentID := fmt.Sprintf("llm-%d", len(k.actorSystem.ListAgents())+1)
|
|
llmOpts := []actor.LLMAgentOption{
|
|
actor.WithToolManager(k.toolMgr),
|
|
actor.WithToolWorker(k.toolWorker),
|
|
actor.WithWindowSize(k.config.Session.MaxHistory),
|
|
}
|
|
|
|
if k.sessionMgr != nil {
|
|
sessionID := "default"
|
|
if _, err := k.sessionMgr.GetSession(sessionID); err != nil {
|
|
k.sessionMgr.CreateSession(sessionID, map[string]string{
|
|
"source": "kernel",
|
|
})
|
|
}
|
|
|
|
llmOpts = append(llmOpts,
|
|
actor.WithSessionManager(k.sessionMgr),
|
|
actor.WithSessionID(sessionID),
|
|
)
|
|
}
|
|
|
|
llmAgent := actor.NewLLMAgent(llmAgentID, ollama, llmOpts...)
|
|
k.llmAgent = llmAgent
|
|
|
|
// Register LLM agent as orchestrator's worker
|
|
k.orch.AddWorker(llmAgent)
|
|
|
|
// Also register tool worker as a fallback worker
|
|
k.orch.AddWorker(tw)
|
|
}
|
|
|
|
func (k *Kernel) createLLMBackend() llm.LLM {
|
|
switch k.config.Provider {
|
|
case config.ProviderDeepSeek:
|
|
return k.createDeepSeekBackend()
|
|
default:
|
|
return k.createOllamaBackend()
|
|
}
|
|
}
|
|
|
|
func (k *Kernel) createOllamaBackend() llm.LLM {
|
|
baseURL := k.config.Ollama.BaseURL
|
|
model := k.config.Ollama.Model
|
|
timeout := k.config.Ollama.Timeout
|
|
|
|
if v := os.Getenv("OLLAMA_BASE_URL"); v != "" {
|
|
baseURL = v
|
|
}
|
|
if v := os.Getenv("OLLAMA_MODEL"); v != "" {
|
|
model = v
|
|
}
|
|
if v := os.Getenv("OLLAMA_TIMEOUT"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
timeout = d
|
|
}
|
|
}
|
|
|
|
client := llm.NewOllamaClient(
|
|
llm.WithBaseURL(baseURL),
|
|
llm.WithModel(model),
|
|
llm.WithTimeout(timeout),
|
|
)
|
|
|
|
log.Printf("kernel: created Ollama client (model=%s, url=%s)", model, baseURL)
|
|
return client
|
|
}
|
|
|
|
func (k *Kernel) createDeepSeekBackend() llm.LLM {
|
|
baseURL := k.config.DeepSeek.BaseURL
|
|
model := k.config.DeepSeek.Model
|
|
apiKey := k.config.DeepSeek.APIKey
|
|
timeout := k.config.DeepSeek.Timeout
|
|
|
|
if v := os.Getenv("DEEPSEEK_BASE_URL"); v != "" {
|
|
baseURL = v
|
|
}
|
|
if v := os.Getenv("DEEPSEEK_MODEL"); v != "" {
|
|
model = v
|
|
}
|
|
if v := os.Getenv("DEEPSEEK_API_KEY"); v != "" {
|
|
apiKey = v
|
|
}
|
|
if v := os.Getenv("DEEPSEEK_TIMEOUT"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
timeout = d
|
|
}
|
|
}
|
|
|
|
client := llm.NewDeepSeekClient(
|
|
llm.WithDeepSeekBaseURL(baseURL),
|
|
llm.WithDeepSeekModel(model),
|
|
llm.WithDeepSeekAPIKey(apiKey),
|
|
llm.WithDeepSeekTimeout(timeout),
|
|
)
|
|
|
|
log.Printf("kernel: created DeepSeek client (model=%s)", model)
|
|
return client
|
|
}
|
|
|
|
// Bus returns the kernel's message bus.
|
|
func (k *Kernel) Bus() bus.MessageBus {
|
|
return k.mb
|
|
}
|
|
|
|
// Registry returns the plugin registry.
|
|
func (k *Kernel) Registry() *plugin.Registry {
|
|
return k.registry
|
|
}
|
|
|
|
// SessionManager returns the session manager.
|
|
func (k *Kernel) SessionManager() *session.Manager {
|
|
return k.sessionMgr
|
|
}
|
|
|
|
// ToolManager returns the tool manager.
|
|
func (k *Kernel) ToolManager() *tool.Manager {
|
|
return k.toolMgr
|
|
}
|
|
|
|
// SkillManager returns the skill manager.
|
|
func (k *Kernel) SkillManager() *skill.Manager {
|
|
return k.skillMgr
|
|
}
|
|
|
|
// ActorSystem returns the actor system.
|
|
func (k *Kernel) ActorSystem() *actor.System {
|
|
return k.actorSystem
|
|
}
|
|
|
|
// Orchestrator returns the orchestrator agent.
|
|
func (k *Kernel) Orchestrator() *actor.Orchestrator {
|
|
return k.orch
|
|
}
|
|
|
|
// LLMAgent returns the LLM agent.
|
|
func (k *Kernel) LLMAgent() *actor.LLMAgent {
|
|
return k.llmAgent
|
|
}
|
|
|
|
// SetStreamWriter sets the writer for streaming LLM output.
|
|
func (k *Kernel) SetStreamWriter(w io.Writer) {
|
|
if k.llmAgent != nil {
|
|
k.llmAgent.SetStreamWriter(w)
|
|
}
|
|
}
|
|
|
|
// SendMessage sends a message from a source to the LLM agent.
|
|
//
|
|
// This is the primary public API for interacting with the Orca system.
|
|
// It creates a task request message and sends it through the orchestrator
|
|
// to the LLM agent for processing.
|
|
//
|
|
// Parameters:
|
|
// - from: the sender identifier (e.g., "user", "cli")
|
|
// - to: the recipient (use "llm" for the LLM agent)
|
|
// - content: the message content (plain text)
|
|
//
|
|
// Returns the response content as a string, or an error.
|
|
func (k *Kernel) SendMessage(from, to, content string) (string, error) {
|
|
if !k.IsRunning() {
|
|
return "", fmt.Errorf("kernel: kernel is not running")
|
|
}
|
|
|
|
if k.orch == nil {
|
|
return "", fmt.Errorf("kernel: orchestrator not initialized")
|
|
}
|
|
|
|
// Create a task request message
|
|
msg := bus.Message{
|
|
Type: bus.MsgTypeTaskRequest,
|
|
From: from,
|
|
To: to,
|
|
Content: content,
|
|
}
|
|
|
|
// Send through the orchestrator
|
|
ctx := context.Background()
|
|
resp, err := k.orch.Process(ctx, msg)
|
|
if err != nil {
|
|
return "", fmt.Errorf("kernel: orchestrator processing failed: %w", err)
|
|
}
|
|
|
|
// Extract response content
|
|
switch v := resp.Content.(type) {
|
|
case string:
|
|
return v, nil
|
|
default:
|
|
return fmt.Sprintf("%v", v), nil
|
|
}
|
|
}
|
|
|
|
// InitPlugins loads and initializes skills from the skills directory.
|
|
func (k *Kernel) InitPlugins() error {
|
|
if k.skillMgr == nil {
|
|
return nil
|
|
}
|
|
|
|
count, err := k.skillMgr.LoadAll()
|
|
if err != nil {
|
|
log.Printf("kernel: warning: skill loading had errors: %v", err)
|
|
}
|
|
if count > 0 {
|
|
log.Printf("kernel: loaded %d skills", count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPlugin returns a registered plugin by name.
|
|
func (k *Kernel) GetPlugin(name string) (plugin.Plugin, bool) {
|
|
return k.registry.Get(name)
|
|
}
|
|
|
|
// ListPlugins returns all currently registered plugins.
|
|
func (k *Kernel) ListPlugins() []plugin.Plugin {
|
|
return k.registry.List()
|
|
}
|
|
|
|
// RegisterPlugin registers a plugin without starting it.
|
|
func (k *Kernel) RegisterPlugin(p plugin.Plugin) error {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
|
|
if k.started {
|
|
return fmt.Errorf("kernel: cannot register plugin %q: kernel already started", p.Name())
|
|
}
|
|
|
|
return k.registry.Register(p)
|
|
}
|
|
|
|
// UnregisterPlugin removes a plugin from the registry.
|
|
func (k *Kernel) UnregisterPlugin(name string) error {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
|
|
return k.registry.Unregister(name)
|
|
}
|
|
|
|
// Start initializes all registered plugins and marks the kernel as running.
|
|
func (k *Kernel) Start() error {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
|
|
if k.started {
|
|
return fmt.Errorf("kernel: already started")
|
|
}
|
|
|
|
k.started = true
|
|
|
|
// Initialize plugins
|
|
plugins := k.registry.List()
|
|
k.plugins = make([]plugin.Plugin, 0, len(plugins))
|
|
|
|
for _, p := range plugins {
|
|
k.registry.SetState(p.Name(), plugin.StateInitialized)
|
|
if err := p.Init(k); err != nil {
|
|
log.Printf("kernel: warning: failed to init plugin %q: %v", p.Name(), err)
|
|
k.registry.SetState(p.Name(), plugin.StateError)
|
|
continue
|
|
}
|
|
k.registry.SetState(p.Name(), plugin.StateRunning)
|
|
k.plugins = append(k.plugins, p)
|
|
log.Printf("kernel: plugin %q (%s) initialized", p.Name(), p.Version())
|
|
}
|
|
|
|
log.Printf("kernel: started (tools=%d)", k.toolMgr.Count())
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully shuts down the kernel.
|
|
func (k *Kernel) Stop() error {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
|
|
if !k.started {
|
|
return nil
|
|
}
|
|
|
|
// Stop actor system first
|
|
if k.actorSystem != nil {
|
|
if err := k.actorSystem.StopAll(); err != nil {
|
|
log.Printf("kernel: warning: error stopping actor system: %v", err)
|
|
}
|
|
}
|
|
|
|
// Stop plugins
|
|
for i := len(k.plugins) - 1; i >= 0; i-- {
|
|
p := k.plugins[i]
|
|
k.registry.SetState(p.Name(), plugin.StateStopped)
|
|
if err := p.Shutdown(); err != nil {
|
|
log.Printf("kernel: warning: error shutting down plugin %q: %v", p.Name(), err)
|
|
continue
|
|
}
|
|
log.Printf("kernel: plugin %q shut down", p.Name())
|
|
}
|
|
|
|
k.plugins = nil
|
|
k.started = false
|
|
|
|
return k.mb.Close()
|
|
}
|
|
|
|
// IsRunning returns whether the kernel has been started and not yet stopped.
|
|
func (k *Kernel) IsRunning() bool {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
return k.started
|
|
}
|