Skip to content

Commit 32c209f

Browse files
feat(api): clarify character-first agent creation (#29)
1 parent 3df3fb6 commit 32c209f

10 files changed

Lines changed: 111 additions & 81 deletions

File tree

README.md

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Go users can also install from source:
4444
go install github.com/snowztech/vikusha/cmd/vikusha@latest
4545
```
4646

47-
### Option 1: YAML
47+
### Character YAML
4848

4949
Create `character.yaml`.
5050

@@ -77,38 +77,20 @@ For structured turn logs, pass `-log-json`. Named agents append logs to `~/.viku
7777
vikusha chat -log-json writer
7878
```
7979

80-
You can load the same YAML from Go.
80+
You can load the same character from Go.
8181

8282
```go
83-
a, err := vikusha.LoadAgent("character.yaml", vikusha.Options{})
83+
a, err := vikusha.LoadAgent("character.yaml", vikusha.BuildOptions{})
8484
reply, err := a.Chat(ctx, "lucas", "hello")
8585
_ = reply
8686

8787
// From another goroutine or transport command:
8888
a.Cancel("lucas")
8989
```
9090

91-
### Option 2: Go
91+
For advanced Go usage where you want to wire provider instances, tool registries, or memory yourself, use `agent.New`; see [examples/file_read](examples/file_read).
9292

93-
Use `agent.New` when you want to pass the provider and tools yourself.
94-
95-
```go
96-
reg := tool.NewRegistry()
97-
reg.Register(file.NewList())
98-
reg.Register(file.NewRead())
99-
100-
a, err := agent.New(agent.Options{
101-
Name: "Helper",
102-
Model: "gpt-4o-mini",
103-
SystemPrompt: "You are helpful.",
104-
Provider: llm.NewOpenAI(os.Getenv("OPENAI_API_KEY")),
105-
Tools: reg,
106-
})
107-
```
108-
109-
Both paths return an `*agent.Agent`; call `Chat(ctx, userID, msg)` to run a turn and `Cancel(userID)` to stop that user's active turn.
110-
111-
## Character YAML
93+
## YAML Fields
11294

11395
The fields implemented today are:
11496

@@ -133,8 +115,8 @@ If `provider` is omitted, Vikusha infers Anthropic for models beginning with `cl
133115
See:
134116

135117
- [examples/from_yaml](examples/from_yaml): create an agent from YAML.
136-
- [examples/hello](examples/hello): create the smallest agent with `agent.New`.
137-
- [examples/file_read](examples/file_read): create an agent with registered file tools.
118+
- [examples/hello](examples/hello): load a character and run one turn.
119+
- [examples/file_read](examples/file_read): advanced manual construction with registered file tools.
138120

139121
---
140122

cmd/vikusha/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func defaultAPIKeyEnv(provider string) string {
201201
}
202202

203203
func buildAgent(path string, logger agent.TurnLogger) (*agent.Agent, error) {
204-
return vikusha.LoadAgent(path, vikusha.Options{
204+
return vikusha.LoadAgent(path, vikusha.BuildOptions{
205205
Workspace: workspaceForCharacter(path),
206206
Logger: logger,
207207
})

core/agent/agent.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type turnCancel struct {
3737
cancel context.CancelFunc
3838
}
3939

40+
// Options contains the runtime dependencies required to construct an Agent
41+
// directly. Most users should prefer vikusha.LoadAgent or vikusha.NewAgent.
4042
type Options struct {
4143
Name string
4244
Model string

docs/ARCHITECTURE.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,18 @@ This document describes Vikusha's target architecture. When a concept is not imp
1010
4. **Flat conceptual model.** Tools, transports, memory backends, and LLM providers are distinct, named concepts.
1111
5. **Character as data.** A YAML file is the entire assistant definition. Same binary, different YAML = different assistant.
1212
6. **Observability over magic.** The user should be able to see which tool ran, which model was called, and which memory was injected.
13-
7. **Two modes of use.** Go framework for developers who embed Vikusha, YAML runtime for configured assistants.
13+
7. **Character-first creation.** A character definition is built into the agent runtime. YAML is the normal user-facing format.
1414

1515
## Target System Flow
1616

17-
Vikusha has two entry paths that meet at the same agent loop:
17+
Vikusha's normal path is character YAML into the agent loop:
1818

1919
```mermaid
2020
flowchart LR
2121
yaml[character.yaml] --> loader[character loader]
2222
cli[vikusha CLI] --> loader
23-
app[Go app] --> manual[agent.New options]
2423
loader --> build[provider + tools + memory]
25-
manual --> build
24+
manual[advanced agent.New] --> agent[Agent]
2625
build --> agent[Agent]
2726
transport[CLI / future transports] --> chat[agent.Chat(ctx, userID, msg)]
2827
agent --> chat
@@ -34,7 +33,7 @@ flowchart LR
3433
tools -- no --> reply[reply]
3534
```
3635

37-
The framework path is explicit Go construction: the caller passes providers, tools, and memory. The runtime path starts from YAML and lets Vikusha wire those pieces. Both paths produce the same `Agent` runtime.
36+
The primary path is character-first: YAML describes the assistant, and Vikusha wires providers, tools, memory, workspace, and logging. The lower-level `agent.New` path remains available for advanced Go users who want to pass runtime dependencies themselves.
3837

3938
## Core concepts
4039

docs/NORTHSTAR.md

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,9 @@ Vikusha is a Go framework and runtime for always-on AI assistants.
44

55
The goal is simple: define an assistant once, then run it wherever people need it.
66

7-
## Two Paths
7+
## Core Idea
88

9-
### Go framework
10-
11-
Go developers can import Vikusha, wire providers and tools, and call:
12-
13-
```go
14-
reply, err := agent.Chat(ctx, userID, msg)
15-
```
16-
17-
This path is for custom binaries, SaaS backends, internal tools, and tests.
18-
19-
### YAML runtime
20-
21-
Users can define an assistant in a character YAML and run it with the `vikusha` CLI.
22-
23-
Today that starts with:
9+
Vikusha turns a character into a runnable agent. The normal user-facing character format is YAML:
2410

2511
```bash
2612
vikusha chat character.yaml
@@ -36,6 +22,8 @@ vikusha chat writer
3622

3723
In that model, `writer` is backed by a character YAML and has its own tools, memory, workspace, logs, secrets, and transports.
3824

25+
Go developers can load the same character from code, or use lower-level APIs when they need custom wiring.
26+
3927
## Always-On Assistants
4028

4129
Always-on means the assistant is a long-running process, not just a one-off prompt.

examples/file_read/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// file_read shows an agent using a tool. The agent is given the file_read
2-
// tool and asked a question that requires reading a file to answer.
1+
// file_read shows advanced manual construction. The agent is given file tools
2+
// directly and asked a question that requires reading a file to answer.
33
//
44
// OPENROUTER_API_KEY=... go run ./examples/file_read
55
package main
@@ -26,6 +26,7 @@ func main() {
2626
}
2727

2828
reg := tool.NewRegistry()
29+
reg.Register(file.NewList())
2930
reg.Register(file.NewRead())
3031

3132
a, err := agent.New(agent.Options{

examples/from_yaml/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
func main() {
1818
_ = godotenv.Load()
1919

20-
a, err := vikusha.LoadAgent("examples/character.yaml", vikusha.Options{})
20+
a, err := vikusha.LoadAgent("examples/character.yaml", vikusha.BuildOptions{})
2121
if err != nil {
2222
log.Fatal(err)
2323
}

examples/hello/main.go

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// hello shows the smallest possible vikusha agent: no tools, one turn,
2-
// answer a question.
1+
// hello shows the normal Go path: load a character YAML and run one turn.
32
//
43
// OPENROUTER_API_KEY=... go run ./examples/hello
54
package main
@@ -8,27 +7,15 @@ import (
87
"context"
98
"fmt"
109
"log"
11-
"os"
1210
"time"
1311

1412
"github.com/joho/godotenv"
15-
"github.com/snowztech/vikusha/core/agent"
16-
"github.com/snowztech/vikusha/core/llm"
13+
"github.com/snowztech/vikusha"
1714
)
1815

1916
func main() {
2017
_ = godotenv.Load()
21-
apiKey := os.Getenv("OPENROUTER_API_KEY")
22-
if apiKey == "" {
23-
log.Fatal("OPENROUTER_API_KEY not set")
24-
}
25-
26-
a, err := agent.New(agent.Options{
27-
Name: "hello",
28-
Model: "openai/gpt-4o-mini",
29-
SystemPrompt: "You are a friendly assistant. Reply in one short sentence.",
30-
Provider: llm.NewOpenRouter(apiKey),
31-
})
18+
a, err := vikusha.LoadAgent("examples/character.yaml", vikusha.BuildOptions{})
3219
if err != nil {
3320
log.Fatal(err)
3421
}

vikusha.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,28 @@ import (
1414
"github.com/snowztech/vikusha/core/tools/file"
1515
)
1616

17-
type Options struct {
18-
Env func(string) string
19-
Tools map[string]tool.Tool
20-
Workspace string
21-
ToolResultCap int
22-
Logger agent.TurnLogger
17+
// BuildOptions controls how Vikusha wires character YAML into a runtime agent.
18+
//
19+
// The character file describes the assistant. BuildOptions describes the host
20+
// process: env lookup, custom tool implementations, workspace, logging, and
21+
// runtime limits.
22+
type BuildOptions struct {
23+
Env func(string) string
24+
AvailableTools map[string]tool.Tool
25+
Workspace string
26+
ToolResultCap int
27+
Logger agent.TurnLogger
2328
}
2429

25-
func LoadAgent(path string, opts Options) (*agent.Agent, error) {
30+
func LoadAgent(path string, opts BuildOptions) (*agent.Agent, error) {
2631
c, err := character.Load(path)
2732
if err != nil {
2833
return nil, err
2934
}
30-
return NewAgent(c, opts)
35+
return newAgent(c, opts)
3136
}
3237

33-
func NewAgent(c *character.Character, opts Options) (*agent.Agent, error) {
38+
func newAgent(c *character.Character, opts BuildOptions) (*agent.Agent, error) {
3439
p, err := provider(c, opts)
3540
if err != nil {
3641
return nil, err
@@ -55,7 +60,7 @@ func NewAgent(c *character.Character, opts Options) (*agent.Agent, error) {
5560
})
5661
}
5762

58-
func provider(c *character.Character, opts Options) (llm.Provider, error) {
63+
func provider(c *character.Character, opts BuildOptions) (llm.Provider, error) {
5964
lookup := opts.Env
6065
if lookup == nil {
6166
lookup = os.Getenv
@@ -95,13 +100,13 @@ func buildMemory(c *character.Character) (memory.Memory, error) {
95100
}
96101
}
97102

98-
func registry(names []string, opts Options) (*tool.Registry, error) {
103+
func registry(names []string, opts BuildOptions) (*tool.Registry, error) {
99104
reg := tool.NewRegistry()
100105
available := map[string]tool.Tool{
101106
"file_list": file.NewList(opts.Workspace),
102107
"file_read": file.NewRead(opts.Workspace),
103108
}
104-
for name, t := range opts.Tools {
109+
for name, t := range opts.AvailableTools {
105110
available[name] = t
106111
}
107112

vikusha_test.go

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"path/filepath"
88
"strings"
99
"testing"
10+
11+
"github.com/snowztech/vikusha/core/character"
12+
"github.com/snowztech/vikusha/core/tool"
1013
)
1114

1215
func TestLoadAgentFromCharacter(t *testing.T) {
@@ -21,7 +24,7 @@ tools:
2124
t.Fatal(err)
2225
}
2326

24-
a, err := LoadAgent(path, Options{
27+
a, err := LoadAgent(path, BuildOptions{
2528
Env: func(name string) string {
2629
if name == "OPENAI_API_KEY" {
2730
return "test-key"
@@ -37,6 +40,69 @@ tools:
3740
}
3841
}
3942

43+
func TestBuildAgentFromCharacter(t *testing.T) {
44+
a, err := newAgent(&character.Character{
45+
Name: "Helper",
46+
Model: "gpt-4o-mini",
47+
SystemPrompt: "Be useful.",
48+
Provider: character.ProviderConfig{
49+
Name: "openai",
50+
APIKeyEnv: "OPENAI_API_KEY",
51+
},
52+
Tools: []string{"file_list", "file_read"},
53+
}, BuildOptions{
54+
Env: func(name string) string {
55+
if name == "OPENAI_API_KEY" {
56+
return "test-key"
57+
}
58+
return ""
59+
},
60+
})
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
if a.Name() != "Helper" {
65+
t.Fatalf("agent name = %q, want Helper", a.Name())
66+
}
67+
}
68+
69+
type customTool struct{}
70+
71+
func (customTool) Name() string { return "custom" }
72+
73+
func (customTool) Description() string { return "custom tool" }
74+
75+
func (customTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
76+
77+
func (customTool) Run(ctx context.Context, input json.RawMessage) (string, error) {
78+
return "ok", nil
79+
}
80+
81+
func TestBuildAgentUsesAvailableTools(t *testing.T) {
82+
a, err := newAgent(&character.Character{
83+
Name: "Helper",
84+
Model: "gpt-4o-mini",
85+
SystemPrompt: "Be useful.",
86+
Tools: []string{"custom"},
87+
}, BuildOptions{
88+
Env: func(name string) string {
89+
if name == "OPENAI_API_KEY" {
90+
return "test-key"
91+
}
92+
return ""
93+
},
94+
AvailableTools: map[string]tool.Tool{
95+
"custom": customTool{},
96+
},
97+
})
98+
if err != nil {
99+
t.Fatal(err)
100+
}
101+
if a.Name() != "Helper" {
102+
t.Fatalf("agent name = %q, want Helper", a.Name())
103+
}
104+
}
105+
40106
func TestLoadAgentRequiresFileMemoryPath(t *testing.T) {
41107
path := filepath.Join(t.TempDir(), "character.yaml")
42108
if err := os.WriteFile(path, []byte(`
@@ -49,7 +115,7 @@ memory:
49115
t.Fatal(err)
50116
}
51117

52-
_, err := LoadAgent(path, Options{
118+
_, err := LoadAgent(path, BuildOptions{
53119
Env: func(name string) string {
54120
if name == "OPENAI_API_KEY" {
55121
return "test-key"
@@ -78,7 +144,7 @@ func TestRegistryScopesBuiltInFileToolsToWorkspace(t *testing.T) {
78144
t.Fatal(err)
79145
}
80146

81-
reg, err := registry([]string{"file_list", "file_read"}, Options{Workspace: workspace})
147+
reg, err := registry([]string{"file_list", "file_read"}, BuildOptions{Workspace: workspace})
82148
if err != nil {
83149
t.Fatal(err)
84150
}

0 commit comments

Comments
 (0)