Skip to content

Commit 0ccc14c

Browse files
committed
feat: v0.1.0 — autonomous ReAct agent loop in Go
Core packages: - kode.go — public API: New(), Run() with OpenAI-compatible config - internal/llm — zero-dep HTTP client for chat completions - internal/loop — ReAct engine: think, act, observe, repeat - internal/tool — thread-safe tool registry - cmd/kode — CLI: kode run, kode version Tested end-to-end with Deepseek: file reading via shell tool confirmed working. Zero external dependencies. stdlib only.
0 parents  commit 0ccc14c

8 files changed

Lines changed: 639 additions & 0 deletions

File tree

bin/kode

8.58 MB
Binary file not shown.

cmd/kode/main.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"strings"
9+
10+
"github.com/BackendStack21/kode"
11+
)
12+
13+
const defaultSystem = `You are kode, an autonomous AI coding agent. You solve tasks by reasoning step by step, then executing tools.
14+
15+
Rules:
16+
1. Think before acting. Explain your reasoning.
17+
2. When you need information, use the shell tool to read files, list directories, or run commands.
18+
3. After gathering information, produce a final answer with no further tool calls.
19+
4. Be concise. Answer the question, then stop.`
20+
21+
func main() {
22+
if len(os.Args) < 2 {
23+
printUsage()
24+
os.Exit(1)
25+
}
26+
27+
switch os.Args[1] {
28+
case "run":
29+
runCmd()
30+
case "version":
31+
fmt.Println("kode v0.1.0")
32+
default:
33+
fmt.Fprintf(os.Stderr, "kode: unknown command %q\n", os.Args[1])
34+
printUsage()
35+
os.Exit(1)
36+
}
37+
}
38+
39+
func printUsage() {
40+
fmt.Println(`Usage:
41+
kode run [flags] <task>
42+
kode version
43+
44+
Flags:
45+
--model <name> LLM model (default: deepseek-chat)
46+
--base-url <url> API endpoint (default: https://api.deepseek.com/v1)
47+
--max-iter <n> Max think->act cycles (default: 90)
48+
--sandbox Run in isolated Docker container
49+
--system <prompt> System prompt override`)
50+
}
51+
52+
func runCmd() {
53+
args := os.Args[2:]
54+
var model, baseURL, system string
55+
maxIter := 90
56+
sandbox := false
57+
58+
i := 0
59+
for i < len(args)-1 {
60+
switch args[i] {
61+
case "--model":
62+
model = args[i+1]
63+
i += 2
64+
case "--base-url":
65+
baseURL = args[i+1]
66+
i += 2
67+
case "--max-iter":
68+
fmt.Sscanf(args[i+1], "%d", &maxIter)
69+
i += 2
70+
case "--system":
71+
system = args[i+1]
72+
i += 2
73+
case "--sandbox":
74+
sandbox = true
75+
i++
76+
default:
77+
// Not a flag — treat remaining as the task
78+
goto done
79+
}
80+
}
81+
done:
82+
task := strings.Join(args[i:], " ")
83+
if task == "" {
84+
fmt.Fprintln(os.Stderr, "kode: no task provided")
85+
os.Exit(1)
86+
}
87+
88+
if system == "" {
89+
system = defaultSystem
90+
}
91+
92+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
93+
defer cancel()
94+
95+
agent, err := kode.New(kode.Config{
96+
Model: model,
97+
BaseURL: baseURL,
98+
MaxIterations: maxIter,
99+
SystemMessage: system,
100+
Tools: builtinTools(),
101+
})
102+
if err != nil {
103+
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
104+
os.Exit(1)
105+
}
106+
107+
_ = sandbox // TODO
108+
109+
modelName := model
110+
if modelName == "" {
111+
modelName = "deepseek-chat"
112+
}
113+
fmt.Fprintf(os.Stderr, "kode: %s thinking...\n", modelName)
114+
115+
result, err := agent.Run(ctx, task)
116+
if err != nil {
117+
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
118+
os.Exit(1)
119+
}
120+
fmt.Println(result)
121+
}
122+
123+
func builtinTools() []kode.Tool {
124+
return []kode.Tool{
125+
&shellTool{},
126+
}
127+
}

cmd/kode/shell.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"os/exec"
8+
"strings"
9+
)
10+
11+
// shellTool lets the agent run shell commands.
12+
type shellTool struct{}
13+
14+
func (t *shellTool) Name() string { return "shell" }
15+
func (t *shellTool) Description() string { return "Run a shell command and return its output. Use for: reading files, listing directories, running tests, building code, git operations. The command runs in the current working directory." }
16+
func (t *shellTool) Schema() any {
17+
return map[string]any{
18+
"type": "object",
19+
"properties": map[string]any{
20+
"command": map[string]any{
21+
"type": "string",
22+
"description": "The shell command to execute",
23+
},
24+
},
25+
"required": []string{"command"},
26+
}
27+
}
28+
29+
func (t *shellTool) Call(args string) (string, error) {
30+
var input struct {
31+
Command string `json:"command"`
32+
}
33+
if err := json.Unmarshal([]byte(args), &input); err != nil {
34+
return "", fmt.Errorf("shell: parse args: %w", err)
35+
}
36+
if input.Command == "" {
37+
return "", fmt.Errorf("shell: empty command")
38+
}
39+
40+
cmd := exec.Command("sh", "-c", input.Command)
41+
var outBuf, errBuf bytes.Buffer
42+
cmd.Stdout = &outBuf
43+
cmd.Stderr = &errBuf
44+
45+
err := cmd.Run()
46+
output := strings.TrimSpace(outBuf.String())
47+
if errBuf.Len() > 0 {
48+
if output != "" {
49+
output += "\n"
50+
}
51+
output += strings.TrimSpace(errBuf.String())
52+
}
53+
if err != nil && output == "" {
54+
return "", fmt.Errorf("shell: %w", err)
55+
}
56+
if output == "" {
57+
output = "(no output)"
58+
}
59+
return output, nil
60+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/BackendStack21/kode
2+
3+
go 1.24.3

internal/llm/client.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Package llm provides an OpenAI-compatible HTTP client using only stdlib.
2+
package llm
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"strings"
12+
"time"
13+
)
14+
15+
// Client sends chat completion requests to any OpenAI-compatible endpoint.
16+
type Client struct {
17+
BaseURL string
18+
APIKey string
19+
Model string
20+
http *http.Client
21+
}
22+
23+
// New creates a Client with sensible defaults.
24+
func New(baseURL, apiKey, model string) *Client {
25+
return &Client{
26+
BaseURL: strings.TrimRight(baseURL, "/"),
27+
APIKey: apiKey,
28+
Model: model,
29+
http: &http.Client{Timeout: 120 * time.Second},
30+
}
31+
}
32+
33+
// Message represents a chat message.
34+
type Message struct {
35+
Role string `json:"role"` // "system", "user", "assistant", "tool"
36+
Content string `json:"content"` // text content
37+
Name string `json:"name,omitempty"` // tool name (for tool role)
38+
ToolCallID string `json:"tool_call_id,omitempty"` // required for tool role
39+
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // required for assistant role with tool calls
40+
}
41+
42+
// ToolCall represents a single tool invocation requested by the model.
43+
// Matches the OpenAI API format exactly.
44+
type ToolCall struct {
45+
ID string `json:"id"`
46+
Type string `json:"type"` // always "function"
47+
Function struct {
48+
Name string `json:"name"`
49+
Arguments string `json:"arguments"`
50+
} `json:"function"`
51+
}
52+
53+
// ToolDef is the JSON Schema definition of a tool.
54+
type ToolDef struct {
55+
Type string `json:"type"`
56+
Function FunctionDef `json:"function"`
57+
}
58+
59+
// FunctionDef defines a single tool's function signature.
60+
type FunctionDef struct {
61+
Name string `json:"name"`
62+
Description string `json:"description"`
63+
Parameters any `json:"parameters"`
64+
}
65+
66+
// CallParams is the request body for /chat/completions.
67+
type CallParams struct {
68+
Model string `json:"model"`
69+
Messages []Message `json:"messages"`
70+
Tools []ToolDef `json:"tools,omitempty"`
71+
Stream bool `json:"stream"`
72+
}
73+
74+
// CallResult is the parsed response from /chat/completions.
75+
type CallResult struct {
76+
Content string // assistant text
77+
ToolCalls []ToolCall // tool calls requested by the model
78+
}
79+
80+
// toolChoiceNone forces the model to not call tools.
81+
var toolChoiceNone = "none"
82+
83+
// Call sends a chat completion request and returns the result.
84+
func (c *Client) Call(ctx context.Context, messages []Message, tools []ToolDef) (*CallResult, error) {
85+
body := CallParams{
86+
Model: c.Model,
87+
Messages: messages,
88+
Tools: tools,
89+
Stream: false,
90+
}
91+
92+
reqBytes, err := json.Marshal(body)
93+
if err != nil {
94+
return nil, fmt.Errorf("llm: marshal request: %w", err)
95+
}
96+
97+
url := c.BaseURL + "/chat/completions"
98+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBytes))
99+
if err != nil {
100+
return nil, fmt.Errorf("llm: create request: %w", err)
101+
}
102+
req.Header.Set("Content-Type", "application/json")
103+
req.Header.Set("Authorization", "Bearer "+c.APIKey)
104+
105+
resp, err := c.http.Do(req)
106+
if err != nil {
107+
return nil, fmt.Errorf("llm: %w", err)
108+
}
109+
defer resp.Body.Close()
110+
111+
respBytes, err := io.ReadAll(resp.Body)
112+
if err != nil {
113+
return nil, fmt.Errorf("llm: read response: %w", err)
114+
}
115+
116+
if resp.StatusCode != http.StatusOK {
117+
return nil, fmt.Errorf("llm: %s (status %d): %s", resp.Status, resp.StatusCode, string(respBytes))
118+
}
119+
120+
return parseResponse(respBytes)
121+
}
122+
123+
func parseResponse(data []byte) (*CallResult, error) {
124+
var raw struct {
125+
Choices []struct {
126+
Message struct {
127+
Content string `json:"content"`
128+
ToolCalls []struct {
129+
ID string `json:"id"`
130+
Function struct {
131+
Name string `json:"name"`
132+
Arguments string `json:"arguments"`
133+
} `json:"function"`
134+
} `json:"tool_calls"`
135+
} `json:"message"`
136+
} `json:"choices"`
137+
}
138+
if err := json.Unmarshal(data, &raw); err != nil {
139+
return nil, fmt.Errorf("llm: parse response: %w (body: %s)", err, string(data))
140+
}
141+
if len(raw.Choices) == 0 {
142+
return nil, fmt.Errorf("llm: no choices in response")
143+
}
144+
145+
msg := raw.Choices[0].Message
146+
result := &CallResult{
147+
Content: msg.Content,
148+
}
149+
for _, tc := range msg.ToolCalls {
150+
result.ToolCalls = append(result.ToolCalls, ToolCall{
151+
ID: tc.ID,
152+
Type: "function",
153+
Function: struct {
154+
Name string `json:"name"`
155+
Arguments string `json:"arguments"`
156+
}{
157+
Name: tc.Function.Name,
158+
Arguments: tc.Function.Arguments,
159+
},
160+
})
161+
}
162+
return result, nil
163+
}

0 commit comments

Comments
 (0)