Core features: - Microkernel architecture with Actor model - Session management with JSONL persistence - Tool system (5 built-in tools) - Skill system with SKILL.md parsing - Sandbox security execution - Ollama integration with gemma4:e4b - Prompt-based tool calling (compatible with native function calling) - REPL interface 11 packages, all tests passing
154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
package actor
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/orca/orca/pkg/bus"
|
|
"github.com/orca/orca/pkg/tool"
|
|
)
|
|
|
|
// ToolWorker is an agent that processes tool call messages by executing
|
|
// tools through the tool.Manager.
|
|
//
|
|
// It implements the Agent interface and handles MsgTypeToolCall messages.
|
|
// When a tool call is received, it extracts the tool name and arguments
|
|
// from the message content, executes the tool via the Manager, and
|
|
// returns a MsgTypeToolResult with the execution result.
|
|
type ToolWorker struct {
|
|
*BaseAgent
|
|
manager *tool.Manager
|
|
}
|
|
|
|
// NewToolWorker creates a new ToolWorker agent with the given id and tool manager.
|
|
// The agent is started automatically upon creation.
|
|
func NewToolWorker(id string, manager *tool.Manager) *ToolWorker {
|
|
w := &ToolWorker{
|
|
BaseAgent: NewBaseAgent(id, "tool_worker"),
|
|
manager: manager,
|
|
}
|
|
w.SetHandler(w.handleMessage)
|
|
if err := w.Start(); err != nil {
|
|
panic(fmt.Sprintf("tool_worker: failed to start: %v", err))
|
|
}
|
|
return w
|
|
}
|
|
|
|
// handleMessage routes incoming messages to the appropriate handler.
|
|
func (w *ToolWorker) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) {
|
|
switch msg.Type {
|
|
case bus.MsgTypeToolCall:
|
|
return w.handleToolCall(ctx, msg)
|
|
case bus.MsgTypeTaskRequest:
|
|
return w.handleTask(ctx, msg)
|
|
case bus.MsgTypeSystem:
|
|
return w.handleSystem(ctx, msg)
|
|
default:
|
|
return bus.Message{}, fmt.Errorf("tool_worker %s: unsupported message type %s", w.ID(), msg.Type)
|
|
}
|
|
}
|
|
|
|
// handleToolCall processes a tool call by executing the named tool
|
|
// with the provided arguments.
|
|
//
|
|
// The msg.Content is expected to contain a JSON object with:
|
|
// - "name": the tool name (string)
|
|
// - "arguments": the tool arguments (object)
|
|
//
|
|
// Or alternatively, msg.Content can be a string in the format:
|
|
// tool_name(arg1=val1, arg2=val2)
|
|
func (w *ToolWorker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Message, error) {
|
|
w.setStatus(StatusWaitingForTool)
|
|
defer w.setStatus(StatusProcessing)
|
|
|
|
toolName, args, err := parseToolCallContent(msg.Content)
|
|
if err != nil {
|
|
return bus.Message{
|
|
ID: msg.ID + "-result",
|
|
Type: bus.MsgTypeToolResult,
|
|
From: w.ID(),
|
|
To: msg.From,
|
|
Content: map[string]interface{}{"error": err.Error()},
|
|
}, nil
|
|
}
|
|
|
|
// Execute the tool
|
|
result, err := w.manager.Execute(toolName, ctx, args)
|
|
if err != nil {
|
|
return bus.Message{
|
|
ID: msg.ID + "-result",
|
|
Type: bus.MsgTypeToolResult,
|
|
From: w.ID(),
|
|
To: msg.From,
|
|
Content: map[string]interface{}{"error": err.Error()},
|
|
}, nil
|
|
}
|
|
|
|
return bus.Message{
|
|
ID: msg.ID + "-result",
|
|
Type: bus.MsgTypeToolResult,
|
|
From: w.ID(),
|
|
To: msg.From,
|
|
Content: result,
|
|
}, nil
|
|
}
|
|
|
|
// parseToolCallContent extracts the tool name and arguments from various
|
|
// content formats.
|
|
func parseToolCallContent(content interface{}) (string, map[string]interface{}, error) {
|
|
switch v := content.(type) {
|
|
case map[string]interface{}:
|
|
// Format: {"name": "tool_name", "arguments": {...}}
|
|
name, ok := v["name"].(string)
|
|
if !ok || name == "" {
|
|
return "", nil, fmt.Errorf("tool call content missing 'name' field")
|
|
}
|
|
args, _ := v["arguments"].(map[string]interface{})
|
|
if args == nil {
|
|
args = make(map[string]interface{})
|
|
}
|
|
return name, args, nil
|
|
|
|
case string:
|
|
// Try JSON format
|
|
var parsed map[string]interface{}
|
|
if err := json.Unmarshal([]byte(v), &parsed); err == nil {
|
|
name, ok := parsed["name"].(string)
|
|
if ok && name != "" {
|
|
args, _ := parsed["arguments"].(map[string]interface{})
|
|
if args == nil {
|
|
args = make(map[string]interface{})
|
|
}
|
|
return name, args, nil
|
|
}
|
|
}
|
|
return "", nil, fmt.Errorf("cannot parse tool call from string content: %s", v)
|
|
|
|
default:
|
|
return "", nil, fmt.Errorf("unsupported tool call content type: %T", content)
|
|
}
|
|
}
|
|
|
|
// handleTask processes a task request by returning a task response.
|
|
func (w *ToolWorker) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) {
|
|
return bus.Message{
|
|
ID: msg.ID + "-response",
|
|
Type: bus.MsgTypeTaskResponse,
|
|
From: w.ID(),
|
|
To: msg.From,
|
|
Content: msg.Content,
|
|
}, nil
|
|
}
|
|
|
|
// handleSystem processes internal system messages.
|
|
func (w *ToolWorker) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) {
|
|
return bus.Message{
|
|
ID: msg.ID + "-ack",
|
|
Type: bus.MsgTypeSystem,
|
|
From: w.ID(),
|
|
To: msg.From,
|
|
Content: "tool_worker acknowledged",
|
|
}, nil
|
|
}
|