diff --git a/cmd/claw/main.go b/cmd/claw/main.go new file mode 100644 index 0000000..4ec07c9 --- /dev/null +++ b/cmd/claw/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "log" + "os" + + "go-tiny-claw/internal/engine" + "go-tiny-claw/internal/schema" +) + +// ========================================== +// 1. 伪造的大模型 Provider +// ========================================== +type mockProvider struct { + turn int +} + +// 模拟大模型的响应:第一轮请求执行 bash,第二轮输出最终结果 +func (m *mockProvider) Generate(ctx context.Context, msgs []schema.Message, _ []schema.ToolDefinition) (*schema.Message, error) { + m.turn++ + if m.turn == 1 { + return &schema.Message{ + Role: schema.RoleAssistant, + Content: "让我来看看当前目录下有什么文件。", + ToolCalls: []schema.ToolCall{ + {ID: "call_123", Name: "bash", Arguments: []byte(`{"command": "ls -la"}`)}, + }, + }, nil + } + + return &schema.Message{ + Role: schema.RoleAssistant, + Content: "我看到了文件列表,里面包含 main.go,任务完成!", + }, nil +} + +// ========================================== +// 2. 伪造的 Tool Registry +// ========================================== +type mockRegistry struct{} + +func (m *mockRegistry) GetAvailableTools() []schema.ToolDefinition { return nil } + +func (m *mockRegistry) Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult { + // 直接返回一段伪造的终端输出 + return schema.ToolResult{ + ToolCallID: call.ID, + Output: "-rw-r--r-- 1 user group 234 Oct 24 10:00 main.go\n", + IsError: false, + } +} + +// ========================================== +// 3. 组装运行 +// ========================================== +func main() { + // 获取当前执行目录作为 WorkDir 物理边界 + workDir, _ := os.Getwd() + + p := &mockProvider{} + r := &mockRegistry{} + + // 实例化核心引擎 + eng := engine.NewAgentEngine(p, r, workDir) + + // 发起任务指令 + err := eng.Run(context.Background(), "帮我检查当前目录的文件") + if err != nil { + log.Fatalf("引擎崩溃: %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5cbf57 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go-tiny-claw + +go 1.24 diff --git a/internal/engine/loop.go b/internal/engine/loop.go new file mode 100644 index 0000000..8aefe9f --- /dev/null +++ b/internal/engine/loop.go @@ -0,0 +1,108 @@ +package engine + +import ( + "context" + "fmt" + "log" + + "go-tiny-claw/internal/provider" + "go-tiny-claw/internal/schema" + "go-tiny-claw/internal/tools" +) + +// AgentEngine 是微型 OS 的核心驱动 +type AgentEngine struct { + provider provider.LLMProvider + registry tools.Registry + + // WorkDir (工作区): 借鉴 OpenClaw 的理念,Agent 必须有一个明确的物理边界 + WorkDir string +} + +func NewAgentEngine(p provider.LLMProvider, r tools.Registry, workDir string) *AgentEngine { + return &AgentEngine{ + provider: p, + registry: r, + WorkDir: workDir, + } +} + +// Run 启动 Agent 的生命周期 +func (e *AgentEngine) Run(ctx context.Context, userPrompt string) error { + log.Printf("[Engine] 引擎启动,锁定工作区: %s\n", e.WorkDir) + + // 1. 初始化会话的 Context (上下文内存) + // 在真实的场景中,这里会由动态 Prompt 组装器加载 AGENTS.md。目前我们先硬编码。 + contextHistory := []schema.Message{ + { + Role: schema.RoleSystem, + Content: "You are go-tiny-claw, an expert coding assistant. You have full access to tools in the workspace.", + }, + { + Role: schema.RoleUser, + Content: userPrompt, + }, + } + + turnCount := 0 + + // 2. The Main Loop: 心跳开始 (标准的 ReAct 循环) + for { + turnCount++ + log.Printf("========== [Turn %d] 开始 ==========\n", turnCount) + + // 获取当前挂载的所有工具定义 + availableTools := e.registry.GetAvailableTools() + + // 向大模型发起推理请求 (包含 Reasoning) + log.Println("[Engine] 正在思考 (Reasoning)...") + responseMsg, err := e.provider.Generate(ctx, contextHistory, availableTools) + if err != nil { + return fmt.Errorf("模型生成失败: %w", err) + } + + // 将模型的响应完整追加到上下文历史中 + contextHistory = append(contextHistory, *responseMsg) + + // 如果模型回复了纯文本,打印出来 (这通常是它的思考过程,或是最终结果) + if responseMsg.Content != "" { + fmt.Printf("🤖 模型: %s\n", responseMsg.Content) + } + + // 3. 退出条件判断 + // 如果模型没有请求任何工具调用,说明它认为任务已经完成,跳出循环。 + if len(responseMsg.ToolCalls) == 0 { + log.Println("[Engine] 任务完成,退出循环。") + break + } + + // 4. 执行行动 (Action) 与 获取观察结果 (Observation) + log.Printf("[Engine] 模型请求调用 %d 个工具...\n", len(responseMsg.ToolCalls)) + + for _, toolCall := range responseMsg.ToolCalls { + log.Printf(" -> 🛠️ 执行工具: %s, 参数: %s\n", toolCall.Name, string(toolCall.Arguments)) + + // 通过 Registry 路由并执行底层工具 + result := e.registry.Execute(ctx, toolCall) + + if result.IsError { + log.Printf(" -> ❌ 工具执行报错: %s\n", result.Output) + } else { + log.Printf(" -> ✅ 工具执行成功 (返回 %d 字节)\n", len(result.Output)) + } + + // 将工具执行的观察结果 (Observation) 封装为 User Message 追加到上下文中 + // 注意:ToolCallID 必须携带!这是维系大模型推理链条的关键 + observationMsg := schema.Message{ + Role: schema.RoleUser, + Content: result.Output, + ToolCallID: toolCall.ID, + } + contextHistory = append(contextHistory, observationMsg) + } + + // 循环回到开头,模型将带着新加入的 Observation 继续它的下一轮思考... + } + + return nil +} diff --git a/internal/provider/LLMProvider.go b/internal/provider/LLMProvider.go new file mode 100644 index 0000000..0c69b76 --- /dev/null +++ b/internal/provider/LLMProvider.go @@ -0,0 +1,12 @@ +package provider + +import ( + "context" + "go-tiny-claw/internal/schema" +) + +// LLMProvider 定义了与大模型通信的统一契约 +type LLMProvider interface { + // Generate 接收当前的上下文历史、可用工具列表,并发起一次大模型推理 + Generate(ctx context.Context, messages []schema.Message, availableTools []schema.ToolDefinition) (*schema.Message, error) +} diff --git a/internal/schema/message.go b/internal/schema/message.go new file mode 100644 index 0000000..610c845 --- /dev/null +++ b/internal/schema/message.go @@ -0,0 +1,46 @@ +package schema + +import "encoding/json" + +// Role 定义消息的角色,这是与大模型沟通的基石 +type Role string + +const ( + RoleSystem Role = "system" // 系统提示词:确立 Agent 的性格与红线 + RoleUser Role = "user" // 用户输入 / 工具执行的返回结果 (Observation) + RoleAssistant Role = "assistant" // 模型的输出:包含推理(Reasoning)或工具调用(ToolCall) +) + +// Message 代表上下文中传递的单条消息 +type Message struct { + Role Role `json:"role"` + Content string `json:"content"` // 存放纯文本内容 + + // 如果模型决定调用工具,此字段将被填充 (支持并行调用多个工具) + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + + // 如果这是对某个工具调用的响应,此字段必须填写,以告知模型上下文的关联性 + ToolCallID string `json:"tool_call_id,omitempty"` +} + +// ToolCall 代表模型请求调用某个具体的工具 +type ToolCall struct { + ID string `json:"id"` // 工具调用的唯一 ID + Name string `json:"name"` // 想要调用的工具名称 (例如 "bash") + // Arguments 存放 JSON 参数。使用 RawMessage 是为了延迟解析,将解析责任交给具体的工具 + Arguments json.RawMessage `json:"arguments"` +} + +// ToolResult 代表工具在本地执行完毕后返回的物理结果 +type ToolResult struct { + ToolCallID string `json:"tool_call_id"` + Output string `json:"output"` // 工具执行的控制台输出或报错堆栈 + IsError bool `json:"is_error"` // 标记是否失败,供后续的驾驭工程进行错误自愈 +} + +// ToolDefinition 描述了一个大模型可以调用的工具元信息 (供模型理解工具有什么用) +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema interface{} `json:"input_schema"` // 对应 JSON Schema +} diff --git a/internal/tools/registry.go b/internal/tools/registry.go new file mode 100644 index 0000000..4d5bebd --- /dev/null +++ b/internal/tools/registry.go @@ -0,0 +1,15 @@ +package tools + +import ( + "context" + "go-tiny-claw/internal/schema" +) + +// Registry 定义了工具的注册与分发执行接口 +type Registry interface { + // GetAvailableTools 返回当前系统挂载的所有可用工具的 Schema + GetAvailableTools() []schema.ToolDefinition + + // Execute 实际执行模型请求的工具,并返回结果 + Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8918222 --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" +) + +//TIP

To run your code, right-click the code and select Run.

Alternatively, click +// the icon in the gutter and select the Run menu item from here.

+ +func main() { + //TIP

Press when your caret is at the underlined text + // to see how GoLand suggests fixing the warning.

Alternatively, if available, click the lightbulb to view possible fixes.

+ s := "gopher" + fmt.Printf("Hello and welcome, %s!\n", s) + + for i := 1; i <= 5; i++ { + //TIP

To start your debugging session, right-click your code in the editor and select the Debug option.

We have set one breakpoint + // for you, but you can always add more by pressing .

+ fmt.Println("i =", 100/i) + } +}