Skip to content

Commit 492b59d

Browse files
grokifyclaude
andcommitted
feat(platforms): add local development platform
Add local platform for running agents without cloud infrastructure. Provides a simple CLI-based interface for development and testing. Features: - Interactive chat with agents via CLI - File system tools (read, write, list) - Bash command execution - Conversation history management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cb47e55 commit 492b59d

File tree

5 files changed

+1544
-0
lines changed

5 files changed

+1544
-0
lines changed

platforms/local/agent.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package local
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// EmbeddedAgent is a lightweight agent that runs in-process.
13+
type EmbeddedAgent struct {
14+
name string
15+
description string
16+
instructions string
17+
tools []Tool
18+
llm LLMClient
19+
maxTokens int
20+
}
21+
22+
// LLMClient defines the interface for language model interactions.
23+
type LLMClient interface {
24+
// Complete generates a completion for the given messages.
25+
Complete(ctx context.Context, messages []Message, tools []ToolDefinition) (*CompletionResponse, error)
26+
}
27+
28+
// Message represents a chat message.
29+
type Message struct {
30+
Role string `json:"role"` // "system", "user", "assistant", "tool"
31+
Content string `json:"content"`
32+
Name string `json:"name,omitempty"` // For tool messages
33+
ToolID string `json:"tool_id,omitempty"` // For tool messages
34+
}
35+
36+
// ToolDefinition defines a tool for the LLM.
37+
type ToolDefinition struct {
38+
Name string `json:"name"`
39+
Description string `json:"description"`
40+
Parameters map[string]interface{} `json:"parameters"`
41+
}
42+
43+
// ToolCall represents an LLM's request to call a tool.
44+
type ToolCall struct {
45+
ID string `json:"id"`
46+
Name string `json:"name"`
47+
Arguments map[string]any `json:"arguments"`
48+
}
49+
50+
// CompletionResponse holds the LLM response.
51+
type CompletionResponse struct {
52+
Content string `json:"content"`
53+
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
54+
Done bool `json:"done"`
55+
}
56+
57+
// NewEmbeddedAgent creates a new embedded agent.
58+
func NewEmbeddedAgent(cfg AgentConfig, toolSet *ToolSet, llm LLMClient) (*EmbeddedAgent, error) {
59+
// Load instructions
60+
instructions := cfg.Instructions
61+
if strings.HasSuffix(cfg.Instructions, ".md") {
62+
content, err := os.ReadFile(cfg.Instructions)
63+
if err != nil {
64+
// Try relative to workspace if absolute path fails
65+
content, err = os.ReadFile(filepath.Join(toolSet.workspace, cfg.Instructions))
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to load instructions: %w", err)
68+
}
69+
}
70+
instructions = string(content)
71+
}
72+
73+
// Create tools
74+
tools, err := toolSet.CreateTools(cfg.Tools)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to create tools: %w", err)
77+
}
78+
79+
maxTokens := cfg.MaxTokens
80+
if maxTokens == 0 {
81+
maxTokens = 4096
82+
}
83+
84+
return &EmbeddedAgent{
85+
name: cfg.Name,
86+
description: cfg.Description,
87+
instructions: instructions,
88+
tools: tools,
89+
llm: llm,
90+
maxTokens: maxTokens,
91+
}, nil
92+
}
93+
94+
// Name returns the agent's name.
95+
func (a *EmbeddedAgent) Name() string {
96+
return a.name
97+
}
98+
99+
// Description returns the agent's description.
100+
func (a *EmbeddedAgent) Description() string {
101+
return a.description
102+
}
103+
104+
// Invoke runs the agent with the given input and returns the result.
105+
func (a *EmbeddedAgent) Invoke(ctx context.Context, input string) (*AgentResult, error) {
106+
// Build initial messages
107+
messages := []Message{
108+
{Role: "system", Content: a.instructions},
109+
{Role: "user", Content: input},
110+
}
111+
112+
// Build tool definitions
113+
toolDefs := a.buildToolDefinitions()
114+
115+
// Agent loop - handle tool calls until done
116+
maxIterations := 10
117+
for i := 0; i < maxIterations; i++ {
118+
// Get completion from LLM
119+
resp, err := a.llm.Complete(ctx, messages, toolDefs)
120+
if err != nil {
121+
return nil, fmt.Errorf("LLM completion failed: %w", err)
122+
}
123+
124+
// If no tool calls, we're done
125+
if len(resp.ToolCalls) == 0 || resp.Done {
126+
return &AgentResult{
127+
Agent: a.name,
128+
Input: input,
129+
Output: resp.Content,
130+
Success: true,
131+
}, nil
132+
}
133+
134+
// Add assistant message with tool calls
135+
messages = append(messages, Message{
136+
Role: "assistant",
137+
Content: resp.Content,
138+
})
139+
140+
// Execute tool calls
141+
for _, tc := range resp.ToolCalls {
142+
result, err := a.executeTool(ctx, tc)
143+
144+
var resultContent string
145+
if err != nil {
146+
resultContent = fmt.Sprintf("Error: %v", err)
147+
} else {
148+
// Marshal result to JSON
149+
resultBytes, _ := json.Marshal(result)
150+
resultContent = string(resultBytes)
151+
}
152+
153+
messages = append(messages, Message{
154+
Role: "tool",
155+
Content: resultContent,
156+
Name: tc.Name,
157+
ToolID: tc.ID,
158+
})
159+
}
160+
}
161+
162+
return &AgentResult{
163+
Agent: a.name,
164+
Input: input,
165+
Output: "Max iterations reached",
166+
Success: false,
167+
Error: "agent loop exceeded maximum iterations",
168+
}, nil
169+
}
170+
171+
// buildToolDefinitions creates tool definitions for the LLM.
172+
func (a *EmbeddedAgent) buildToolDefinitions() []ToolDefinition {
173+
var defs []ToolDefinition
174+
for _, tool := range a.tools {
175+
def := ToolDefinition{
176+
Name: tool.Name(),
177+
Description: tool.Description(),
178+
Parameters: a.getToolParameters(tool.Name()),
179+
}
180+
defs = append(defs, def)
181+
}
182+
return defs
183+
}
184+
185+
// getToolParameters returns the parameter schema for a tool.
186+
func (a *EmbeddedAgent) getToolParameters(name string) map[string]interface{} {
187+
switch name {
188+
case "read":
189+
return map[string]interface{}{
190+
"type": "object",
191+
"properties": map[string]interface{}{
192+
"path": map[string]interface{}{
193+
"type": "string",
194+
"description": "Path to the file to read",
195+
},
196+
},
197+
"required": []string{"path"},
198+
}
199+
case "write":
200+
return map[string]interface{}{
201+
"type": "object",
202+
"properties": map[string]interface{}{
203+
"path": map[string]interface{}{
204+
"type": "string",
205+
"description": "Path to the file to write",
206+
},
207+
"content": map[string]interface{}{
208+
"type": "string",
209+
"description": "Content to write to the file",
210+
},
211+
},
212+
"required": []string{"path", "content"},
213+
}
214+
case "glob":
215+
return map[string]interface{}{
216+
"type": "object",
217+
"properties": map[string]interface{}{
218+
"pattern": map[string]interface{}{
219+
"type": "string",
220+
"description": "Glob pattern to match files",
221+
},
222+
},
223+
"required": []string{"pattern"},
224+
}
225+
case "grep":
226+
return map[string]interface{}{
227+
"type": "object",
228+
"properties": map[string]interface{}{
229+
"pattern": map[string]interface{}{
230+
"type": "string",
231+
"description": "Regex pattern to search for",
232+
},
233+
"file_pattern": map[string]interface{}{
234+
"type": "string",
235+
"description": "Optional file name pattern to filter files",
236+
},
237+
},
238+
"required": []string{"pattern"},
239+
}
240+
case "shell":
241+
return map[string]interface{}{
242+
"type": "object",
243+
"properties": map[string]interface{}{
244+
"command": map[string]interface{}{
245+
"type": "string",
246+
"description": "Shell command to execute",
247+
},
248+
},
249+
"required": []string{"command"},
250+
}
251+
default:
252+
return map[string]interface{}{"type": "object"}
253+
}
254+
}
255+
256+
// executeTool executes a tool call and returns the result.
257+
func (a *EmbeddedAgent) executeTool(ctx context.Context, tc ToolCall) (any, error) {
258+
for _, tool := range a.tools {
259+
if tool.Name() == tc.Name {
260+
return tool.Execute(ctx, tc.Arguments)
261+
}
262+
}
263+
return nil, fmt.Errorf("unknown tool: %s", tc.Name)
264+
}
265+
266+
// AgentResult holds the result of an agent invocation.
267+
type AgentResult struct {
268+
Agent string `json:"agent"`
269+
Input string `json:"input"`
270+
Output string `json:"output"`
271+
Success bool `json:"success"`
272+
Error string `json:"error,omitempty"`
273+
}

0 commit comments

Comments
 (0)