zuzu 0.0.1-java → 0.2.2-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +47 -32
- data/bin/setup +3 -6
- data/bin/zuzu +32 -5
- data/lib/zuzu/agent.rb +18 -11
- data/lib/zuzu/config.rb +4 -3
- data/lib/zuzu/version.rb +1 -1
- data/templates/.claude/skills/add-tool/SKILL.md +162 -0
- data/templates/.claude/skills/customize/SKILL.md +192 -0
- data/templates/.claude/skills/debug/SKILL.md +197 -0
- data/templates/.claude/skills/setup/SKILL.md +102 -0
- data/templates/AGENTS.md +557 -0
- data/templates/CLAUDE.md +70 -0
- data/templates/app.rb +52 -11
- data/warble.rb +19 -0
- metadata +31 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6098f883dec29f87b5dcece9c69f8e115608cfd89b9cf045fd442355fbf48c9
|
|
4
|
+
data.tar.gz: d9ffeb971f19158a6262401bdcadb7791bdbd70523c843dd6b7ac26cc3a4c536
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7d6f2d65abfc635d0dceec2122a788d7d2cc03159969c9e06a1b03a45e73eb9c03bf430d5f1a6170a7aaa8c717961bc5c51311336cdf80ab4a3b28feb1dc8b4
|
|
7
|
+
data.tar.gz: b10535955c154eee0eaade46edb4ee5b1b9049e8e224cea1d49da58553d1bbd1ae263045977f9a1ac6d87dbb5bf0c7da62e26e73eb5ec0c4b3bf8152ba699f58
|
data/README.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# Zuzu
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Build AI-native desktop apps that run entirely on the user's machine.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Every application you install on an operating system does the same fundamental thing: it translates human intent into OS system calls. A text editor writes bytes to disk. A browser opens network connections. At its core, every installed app is an orchestrator of operating system capabilities.
|
|
6
|
+
|
|
7
|
+
LLMs are simply a more expressive interface for exactly that orchestration. Zuzu is a framework built on this premise — for developers who want to ship installable, AI-native desktop apps where the intelligence runs on the user's hardware, not in a data center.
|
|
8
|
+
|
|
9
|
+
**Why does this matter?**
|
|
10
|
+
|
|
11
|
+
- **Privacy by architecture.** The agent operates inside AgentFS — a sandboxed virtual filesystem backed by a single SQLite file. It cannot touch the host OS unless you explicitly open that door. There is no network call to make, no token to rotate, no terms of service that changes next quarter.
|
|
12
|
+
|
|
13
|
+
- **Deployable like software, not like a service.** Package your app as a single `.jar`. Users pay, download, double-click, and run. No Docker. No cloud subscription. No infrastructure to maintain. The JVM handles cross-platform distribution the way it has for 30 years.
|
|
14
|
+
|
|
15
|
+
- **Built for regulated environments.** A therapist keeping session notes, an auditor running confidential analysis, a corporate team in a restricted environment — these are exactly the users who benefit most from powerful AI but are currently blocked by cloud dependency. A bundled LLM in a self-contained Java application needs no external approval to run.
|
|
16
|
+
|
|
17
|
+
- **Developer experience that matches how software is actually built today.** `zuzu new my_app` scaffolds a project pre-wired for Claude Code: CLAUDE.md, skills for `/setup`, `/add-tool`, `/customize`, and `/debug` — all enforcing Zuzu's patterns. Open the folder, start your coding agent, describe what you want to build.
|
|
18
|
+
|
|
19
|
+
→ [Why Zuzu exists](docs/why.md) · [Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
|
|
7
20
|
|
|
8
21
|
<video src="https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4" controls width="100%"></video>
|
|
9
22
|
[Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
|
|
@@ -32,7 +45,7 @@ cd zuzu
|
|
|
32
45
|
bin/setup
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
`bin/setup` installs Java 21, JRuby 10.0.
|
|
48
|
+
`bin/setup` installs Java 21, JRuby 10.0.3.0, and all gem dependencies
|
|
36
49
|
automatically. If you prefer manual setup, see [Manual Setup](#manual-setup)
|
|
37
50
|
below.
|
|
38
51
|
|
|
@@ -61,9 +74,11 @@ Edit `app.rb` to point at your model:
|
|
|
61
74
|
|
|
62
75
|
```ruby
|
|
63
76
|
Zuzu.configure do |c|
|
|
64
|
-
c.app_name
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
c.app_name = 'My Assistant'
|
|
78
|
+
# Works both when run directly and from a packaged .jar
|
|
79
|
+
base = __dir__.to_s.start_with?('uri:classloader:') ? Dir.pwd : __dir__
|
|
80
|
+
c.llamafile_path = File.join(base, 'models', 'llava-v1.5-7b-q4.llamafile')
|
|
81
|
+
c.db_path = File.join(base, '.zuzu', 'zuzu.db')
|
|
67
82
|
c.port = 8080
|
|
68
83
|
end
|
|
69
84
|
```
|
|
@@ -71,16 +86,11 @@ end
|
|
|
71
86
|
Launch:
|
|
72
87
|
|
|
73
88
|
```bash
|
|
74
|
-
|
|
75
|
-
JRUBY_OPTS="-J-XstartOnFirstThread -J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
|
|
76
|
-
|
|
77
|
-
# Linux:
|
|
78
|
-
JRUBY_OPTS="-J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
|
|
89
|
+
bundle exec zuzu start
|
|
79
90
|
```
|
|
80
91
|
|
|
81
|
-
You'll see a native desktop window
|
|
82
|
-
|
|
83
|
-
background.
|
|
92
|
+
You'll see a native desktop chat window. The llamafile model starts automatically
|
|
93
|
+
in the background. Click **Admin Panel** to browse the AgentFS virtual filesystem.
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
|
@@ -90,18 +100,14 @@ If `bin/setup` doesn't suit your workflow, follow these steps.
|
|
|
90
100
|
|
|
91
101
|
### 1. Install Java 21+
|
|
92
102
|
|
|
93
|
-
**macOS (Homebrew):**
|
|
103
|
+
**macOS (Homebrew — recommended):**
|
|
94
104
|
|
|
95
105
|
```bash
|
|
96
|
-
brew install
|
|
106
|
+
brew install --cask temurin@21
|
|
97
107
|
```
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
|
|
103
|
-
export PATH="$JAVA_HOME/bin:$PATH"
|
|
104
|
-
```
|
|
109
|
+
Temurin is the Eclipse/Adoptium OpenJDK distribution. The cask sets up
|
|
110
|
+
`JAVA_HOME` automatically — no manual PATH changes needed.
|
|
105
111
|
|
|
106
112
|
**macOS (SDKMAN):**
|
|
107
113
|
|
|
@@ -125,17 +131,17 @@ java -version
|
|
|
125
131
|
# → openjdk version "21.x.x" ...
|
|
126
132
|
```
|
|
127
133
|
|
|
128
|
-
### 2. Install JRuby 10.0.
|
|
134
|
+
### 2. Install JRuby 10.0.3.0 via rbenv
|
|
129
135
|
|
|
130
136
|
```bash
|
|
131
137
|
# Install rbenv if needed
|
|
132
138
|
brew install rbenv ruby-build # macOS
|
|
133
139
|
# or: https://github.com/rbenv/rbenv#installation
|
|
134
140
|
|
|
135
|
-
rbenv install jruby-10.0.
|
|
136
|
-
rbenv local jruby-10.0.
|
|
141
|
+
rbenv install jruby-10.0.3.0
|
|
142
|
+
rbenv local jruby-10.0.3.0
|
|
137
143
|
ruby -v
|
|
138
|
-
# → jruby 10.0.
|
|
144
|
+
# → jruby 10.0.3.0 (ruby 3.x.x) ...
|
|
139
145
|
```
|
|
140
146
|
|
|
141
147
|
### 3. Install Gems
|
|
@@ -289,15 +295,23 @@ fs.kv_get('last_query') # → "weather in Tokyo"
|
|
|
289
295
|
|
|
290
296
|
### Packaging as .jar
|
|
291
297
|
|
|
292
|
-
|
|
298
|
+
Package your app as a standalone Java archive with a single command:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
bundle exec zuzu package
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This auto-installs Warbler if needed, generates the necessary launcher, and
|
|
305
|
+
produces a `.jar` named after your app directory. Run it with:
|
|
293
306
|
|
|
294
307
|
```bash
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
java -XstartOnFirstThread -jar zuzu-app.jar # macOS
|
|
298
|
-
java -jar zuzu-app.jar # Linux
|
|
308
|
+
java -XstartOnFirstThread -jar my_app.jar # macOS
|
|
309
|
+
java -jar my_app.jar # Linux / Windows
|
|
299
310
|
```
|
|
300
311
|
|
|
312
|
+
> **Note:** Place your llamafile model in a `models/` directory alongside the
|
|
313
|
+
> `.jar` — models are not bundled into the archive.
|
|
314
|
+
|
|
301
315
|
---
|
|
302
316
|
|
|
303
317
|
## Configuration Reference
|
|
@@ -323,6 +337,7 @@ end
|
|
|
323
337
|
```
|
|
324
338
|
zuzu new APP_NAME Scaffold a new Zuzu application
|
|
325
339
|
zuzu start Launch the Zuzu app in the current directory
|
|
340
|
+
zuzu package Package the app as a standalone .jar
|
|
326
341
|
zuzu console Open an IRB session with Zuzu loaded
|
|
327
342
|
zuzu version Print the Zuzu version
|
|
328
343
|
zuzu help Show this message
|
data/bin/setup
CHANGED
|
@@ -20,12 +20,9 @@ if java -version 2>&1 | grep -q 'version "2[1-9]\|version "3'; then
|
|
|
20
20
|
else
|
|
21
21
|
warn "Java 21+ not found."
|
|
22
22
|
if [[ "$OSTYPE" == darwin* ]]; then
|
|
23
|
-
step "Installing Java 21 via Homebrew"
|
|
24
|
-
brew install
|
|
25
|
-
echo
|
|
26
|
-
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
|
|
27
|
-
export PATH="$JAVA_HOME/bin:$PATH"
|
|
28
|
-
echo " Installed. Restart your shell or run: source ~/.zshrc"
|
|
23
|
+
step "Installing Java 21 via Homebrew (Temurin)"
|
|
24
|
+
brew install --cask temurin@21
|
|
25
|
+
echo " Installed. Restart your shell to pick up JAVA_HOME."
|
|
29
26
|
elif [[ "$OSTYPE" == linux-gnu* ]]; then
|
|
30
27
|
step "Installing Java 21 via apt"
|
|
31
28
|
sudo apt-get update -qq && sudo apt-get install -y openjdk-21-jdk
|
data/bin/zuzu
CHANGED
|
@@ -33,10 +33,13 @@ when 'new'
|
|
|
33
33
|
app_dir = File.expand_path(app_name)
|
|
34
34
|
abort "Directory '#{app_name}' already exists." if File.exist?(app_dir)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
templates_dir = File.expand_path('../templates', __dir__)
|
|
37
37
|
|
|
38
38
|
FileUtils.mkdir_p(app_dir)
|
|
39
|
-
FileUtils.cp(
|
|
39
|
+
FileUtils.cp(File.join(templates_dir, 'app.rb'), File.join(app_dir, 'app.rb'))
|
|
40
|
+
FileUtils.cp(File.join(templates_dir, 'AGENTS.md'), File.join(app_dir, 'AGENTS.md'))
|
|
41
|
+
FileUtils.cp(File.join(templates_dir, 'CLAUDE.md'), File.join(app_dir, 'CLAUDE.md'))
|
|
42
|
+
FileUtils.cp_r(File.join(templates_dir, '.claude'), File.join(app_dir, '.claude'))
|
|
40
43
|
|
|
41
44
|
# Generate Gemfile — uses local path when running from source tree,
|
|
42
45
|
# published gem when installed via `gem install zuzu`.
|
|
@@ -55,6 +58,9 @@ when 'new'
|
|
|
55
58
|
puts "Created new Zuzu app: #{app_name}/"
|
|
56
59
|
puts " #{app_name}/app.rb"
|
|
57
60
|
puts " #{app_name}/Gemfile"
|
|
61
|
+
puts " #{app_name}/AGENTS.md"
|
|
62
|
+
puts " #{app_name}/CLAUDE.md"
|
|
63
|
+
puts " #{app_name}/.claude/skills/ (setup, add-tool, customize, debug)"
|
|
58
64
|
puts ''
|
|
59
65
|
if DEV_MODE
|
|
60
66
|
puts " (dev mode: Gemfile points to #{ZUZU_ROOT})"
|
|
@@ -69,6 +75,15 @@ when 'new'
|
|
|
69
75
|
when 'start'
|
|
70
76
|
entry = ['app.rb', 'lib/app.rb'].find { |f| File.exist?(f) }
|
|
71
77
|
abort 'No app.rb found. Run `zuzu start` from your app directory.' unless entry
|
|
78
|
+
|
|
79
|
+
# SWT on Java 21+ requires --enable-native-access=ALL-UNNAMED.
|
|
80
|
+
# JVM flags must be set before the JVM starts, so re-exec via bundle
|
|
81
|
+
# with the flag injected into JRUBY_OPTS if it isn't already present.
|
|
82
|
+
unless ENV['JRUBY_OPTS'].to_s.include?('enable-native-access')
|
|
83
|
+
ENV['JRUBY_OPTS'] = "#{ENV['JRUBY_OPTS']} -J--enable-native-access=ALL-UNNAMED".strip
|
|
84
|
+
exec('bundle', 'exec', 'zuzu', 'start')
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
load File.expand_path(entry)
|
|
73
88
|
|
|
74
89
|
when 'package'
|
|
@@ -96,16 +111,28 @@ when 'package'
|
|
|
96
111
|
abort 'warble binary not found after install. Try: gem install warbler' if warble_bin.empty?
|
|
97
112
|
end
|
|
98
113
|
|
|
114
|
+
# Warbler 2.x auto-discovers the main script from bin/ — create a thin launcher
|
|
115
|
+
unless File.exist?('bin/app')
|
|
116
|
+
FileUtils.mkdir_p('bin')
|
|
117
|
+
File.write('bin/app', <<~SCRIPT)
|
|
118
|
+
#!/usr/bin/env jruby
|
|
119
|
+
load File.expand_path('../app.rb', __dir__)
|
|
120
|
+
SCRIPT
|
|
121
|
+
File.chmod(0o755, 'bin/app')
|
|
122
|
+
puts 'Created bin/app launcher for warbler.'
|
|
123
|
+
end
|
|
124
|
+
|
|
99
125
|
puts 'Packaging app as zuzu-app.jar ...'
|
|
100
126
|
puts ' (this may take a minute)'
|
|
101
127
|
success = Bundler.with_unbundled_env { system(warble_bin, 'jar') }
|
|
102
128
|
if success
|
|
103
129
|
puts ''
|
|
104
|
-
|
|
130
|
+
jar = Dir['*.jar'].first || 'app.jar'
|
|
131
|
+
puts "Done! Created: #{jar}"
|
|
105
132
|
puts ''
|
|
106
133
|
puts 'Run it with:'
|
|
107
|
-
puts
|
|
108
|
-
puts
|
|
134
|
+
puts " java -XstartOnFirstThread -jar #{jar} # macOS"
|
|
135
|
+
puts " java -jar #{jar} # Linux / Windows"
|
|
109
136
|
else
|
|
110
137
|
abort 'warble jar failed. Check output above for details.'
|
|
111
138
|
end
|
data/lib/zuzu/agent.rb
CHANGED
|
@@ -11,22 +11,13 @@ module Zuzu
|
|
|
11
11
|
TOOL_CALL_RE = /<zuzu_tool_call>(.*?)<\/zuzu_tool_call>/m
|
|
12
12
|
TOOL_RESULT_RE = /<zuzu_tool_result>.*?<\/zuzu_tool_result>/m
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
BASE_PROMPT = <<~PROMPT
|
|
15
15
|
You are Zuzu, a helpful desktop AI assistant.
|
|
16
16
|
|
|
17
17
|
You have access to a sandboxed virtual filesystem called AgentFS. It is completely
|
|
18
18
|
separate from the host computer's filesystem. All file paths refer to AgentFS only.
|
|
19
19
|
You cannot access or modify any files on the host system.
|
|
20
20
|
|
|
21
|
-
Available tools — use the tag format shown below:
|
|
22
|
-
|
|
23
|
-
- write_file : Write text to an AgentFS file. Args: path (string), content (string)
|
|
24
|
-
- read_file : Read an AgentFS file. Args: path (string)
|
|
25
|
-
- list_directory : List an AgentFS directory. Args: path (string, default "/")
|
|
26
|
-
- run_command : Run a sandboxed command against AgentFS. Args: command (string)
|
|
27
|
-
Supported: ls [path], cat <path>, pwd, echo <text>
|
|
28
|
-
- http_get : Fetch a public URL from the internet. Args: url (string)
|
|
29
|
-
|
|
30
21
|
To call a tool, output exactly this on its own line:
|
|
31
22
|
<zuzu_tool_call>{"name":"TOOL_NAME","args":{"key":"value"}}</zuzu_tool_call>
|
|
32
23
|
|
|
@@ -51,7 +42,7 @@ module Zuzu
|
|
|
51
42
|
# Only system prompt + current message — no history injected into agent context.
|
|
52
43
|
# Prior non-tool-call responses cause models to skip tool use.
|
|
53
44
|
messages = [
|
|
54
|
-
{ 'role' => 'system', 'content' =>
|
|
45
|
+
{ 'role' => 'system', 'content' => build_system_prompt },
|
|
55
46
|
{ 'role' => 'user', 'content' => user_message }
|
|
56
47
|
]
|
|
57
48
|
|
|
@@ -100,6 +91,22 @@ module Zuzu
|
|
|
100
91
|
|
|
101
92
|
private
|
|
102
93
|
|
|
94
|
+
def build_system_prompt
|
|
95
|
+
tool_lines = ToolRegistry.tools.map do |t|
|
|
96
|
+
args = t.schema[:properties]&.keys&.map(&:to_s)&.join(', ')
|
|
97
|
+
line = "- #{t.name} : #{t.description}"
|
|
98
|
+
line += " Args: #{args}" if args && !args.empty?
|
|
99
|
+
line
|
|
100
|
+
end.join("\n")
|
|
101
|
+
|
|
102
|
+
extras = Zuzu.config.system_prompt_extras.to_s.strip
|
|
103
|
+
|
|
104
|
+
prompt = BASE_PROMPT.dup
|
|
105
|
+
prompt << "\nAvailable tools:\n#{tool_lines}\n"
|
|
106
|
+
prompt << "\n#{extras}" unless extras.empty?
|
|
107
|
+
prompt.strip
|
|
108
|
+
end
|
|
109
|
+
|
|
103
110
|
def extract_tool_calls(content)
|
|
104
111
|
content.scan(TOOL_CALL_RE).filter_map do |match|
|
|
105
112
|
data = JSON.parse(match[0].strip)
|
data/lib/zuzu/config.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Zuzu
|
|
|
11
11
|
#
|
|
12
12
|
class Config
|
|
13
13
|
attr_accessor :port, :model, :channels, :log_level, :app_name,
|
|
14
|
-
:window_width, :window_height
|
|
14
|
+
:window_width, :window_height, :system_prompt_extras
|
|
15
15
|
|
|
16
16
|
attr_reader :db_path, :llamafile_path
|
|
17
17
|
|
|
@@ -20,8 +20,9 @@ module Zuzu
|
|
|
20
20
|
@model = 'LLaMA_CPP'
|
|
21
21
|
@db_path = File.join('.zuzu', 'zuzu.db')
|
|
22
22
|
@llamafile_path = nil
|
|
23
|
-
@channels
|
|
24
|
-
@log_level
|
|
23
|
+
@channels = []
|
|
24
|
+
@log_level = :info
|
|
25
|
+
@system_prompt_extras = nil
|
|
25
26
|
@app_name = 'Zuzu'
|
|
26
27
|
@window_width = 860
|
|
27
28
|
@window_height = 620
|
data/lib/zuzu/version.rb
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-tool
|
|
3
|
+
description: Add a new tool that the Zuzu agent can call during conversations. Guides through naming, arguments, implementation, and registers it correctly in app.rb. Use when the developer wants the AI assistant to gain a new capability (e.g. check weather, query a database, read a file, call an API).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a Zuzu Tool
|
|
7
|
+
|
|
8
|
+
A tool is a Ruby block the agent calls using `<zuzu_tool_call>` tags. Once registered, it is **automatically listed in the agent's system prompt** — no manual prompt editing needed.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Understand what the tool should do
|
|
11
|
+
|
|
12
|
+
AskUserQuestion: "What should this tool do? Describe it in one sentence — this description will be shown to the AI agent."
|
|
13
|
+
|
|
14
|
+
AskUserQuestion: "What arguments does it need? For each: name, type (string/number/boolean), and what it represents. Or 'none' if no arguments."
|
|
15
|
+
|
|
16
|
+
AskUserQuestion: "Does it need to read/write files (use AgentFS), call an external API, query a database, or just compute something locally?"
|
|
17
|
+
|
|
18
|
+
## Step 2 — Choose a tool name
|
|
19
|
+
|
|
20
|
+
- Must be snake_case, descriptive, unambiguous
|
|
21
|
+
- Examples: `get_weather`, `search_notes`, `send_email`, `calculate_tax`, `lookup_stock_price`
|
|
22
|
+
- Check `app.rb` for existing tool names — avoid duplicates
|
|
23
|
+
- AskUserQuestion: "I'll name this tool `<suggested_name>`. Does that work, or would you prefer a different name?"
|
|
24
|
+
|
|
25
|
+
## Step 3 — Implement the tool
|
|
26
|
+
|
|
27
|
+
Read `app.rb` to find the insertion point — tools go **between the `Zuzu.configure` block and `Zuzu::App.launch!`**.
|
|
28
|
+
|
|
29
|
+
### Pattern A — No arguments, no AgentFS
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Zuzu::ToolRegistry.register(
|
|
33
|
+
'tool_name',
|
|
34
|
+
'One-sentence description shown to the agent.',
|
|
35
|
+
{ type: 'object', properties: {}, required: [] }
|
|
36
|
+
) { |_args, _fs| "result as a string" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Pattern B — With arguments
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Zuzu::ToolRegistry.register(
|
|
43
|
+
'tool_name',
|
|
44
|
+
'Description shown to the agent.',
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
param_one: { type: 'string', description: 'what it is' },
|
|
49
|
+
param_two: { type: 'integer', description: 'what it is' }
|
|
50
|
+
},
|
|
51
|
+
required: ['param_one'] # list only truly required params
|
|
52
|
+
}
|
|
53
|
+
) do |args, _fs|
|
|
54
|
+
# args keys are STRINGS: args['param_one'] not args[:param_one]
|
|
55
|
+
value = args['param_one'].to_s
|
|
56
|
+
"result: #{value}"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Pattern C — Using AgentFS (sandboxed file/KV access)
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Zuzu::ToolRegistry.register(
|
|
64
|
+
'save_note',
|
|
65
|
+
'Save a note to the virtual filesystem.',
|
|
66
|
+
{
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
title: { type: 'string', description: 'Note title' },
|
|
70
|
+
content: { type: 'string', description: 'Note content' }
|
|
71
|
+
},
|
|
72
|
+
required: %w[title content]
|
|
73
|
+
}
|
|
74
|
+
) do |args, fs|
|
|
75
|
+
# fs is Zuzu::AgentFS — sandboxed, NOT the host filesystem
|
|
76
|
+
path = "/notes/#{args['title'].downcase.gsub(/\s+/, '_')}.txt"
|
|
77
|
+
fs.write_file(path, args['content'])
|
|
78
|
+
"Saved to #{path}"
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Pattern D — Calling an external HTTP API
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
require 'net/http'
|
|
86
|
+
require 'json'
|
|
87
|
+
|
|
88
|
+
Zuzu::ToolRegistry.register(
|
|
89
|
+
'get_weather',
|
|
90
|
+
'Get current weather for a city using wttr.in.',
|
|
91
|
+
{
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
city: { type: 'string', description: 'City name' }
|
|
95
|
+
},
|
|
96
|
+
required: ['city']
|
|
97
|
+
}
|
|
98
|
+
) do |args, _fs|
|
|
99
|
+
uri = URI("https://wttr.in/#{URI.encode_uri_component(args['city'])}?format=3")
|
|
100
|
+
Net::HTTP.get(uri).strip
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
"Error fetching weather: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Step 4 — Enforce these rules before writing
|
|
107
|
+
|
|
108
|
+
Before inserting code, verify:
|
|
109
|
+
|
|
110
|
+
- [ ] Tool is placed **before** `Zuzu::App.launch!`
|
|
111
|
+
- [ ] Block signature uses `|args, fs|` or `|args, _fs|` (never zero args)
|
|
112
|
+
- [ ] `args` keys use **string** form: `args['name']` not `args[:name]`
|
|
113
|
+
- [ ] Return value is a **String** (or will be `.to_s`'d automatically)
|
|
114
|
+
- [ ] No `File.read` / `File.write` / `Dir` calls — use `fs` for file access
|
|
115
|
+
- [ ] External HTTP calls have a rescue block returning an error string
|
|
116
|
+
- [ ] Description is one clear sentence (the agent sees this verbatim)
|
|
117
|
+
|
|
118
|
+
Write the tool to `app.rb` now.
|
|
119
|
+
|
|
120
|
+
## Step 5 — Verify it loads
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
bundle exec ruby -e "
|
|
124
|
+
require 'zuzu'
|
|
125
|
+
load 'app.rb' rescue nil
|
|
126
|
+
tool = Zuzu::ToolRegistry.find('<tool_name>')
|
|
127
|
+
if tool
|
|
128
|
+
puts 'Tool registered: ' + tool.name
|
|
129
|
+
puts 'Description: ' + tool.description
|
|
130
|
+
else
|
|
131
|
+
puts 'ERROR: tool not found'
|
|
132
|
+
end
|
|
133
|
+
" 2>&1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- If "tool not found": check for syntax errors, verify placement before `launch!`, retry.
|
|
137
|
+
- If syntax error printed: fix it, retry.
|
|
138
|
+
|
|
139
|
+
## Step 6 — Test in console (if possible without side effects)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bundle exec zuzu console
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then in the console:
|
|
146
|
+
```ruby
|
|
147
|
+
store = Zuzu::Store.new
|
|
148
|
+
fs = Zuzu::AgentFS.new(store)
|
|
149
|
+
tool = Zuzu::ToolRegistry.find('<tool_name>')
|
|
150
|
+
puts tool.block.call({'param' => 'test_value'}, fs)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- If it returns a sensible result: done.
|
|
154
|
+
- If error: fix and re-verify.
|
|
155
|
+
|
|
156
|
+
## Step 7 — Tell the user
|
|
157
|
+
|
|
158
|
+
Show the registered tool code and confirm:
|
|
159
|
+
> ✅ Tool `<name>` registered. The agent will now automatically list it in its system prompt and call it when relevant. Restart the app (`bundle exec zuzu start`) to pick up the change.
|
|
160
|
+
|
|
161
|
+
If the tool calls an external service that needs configuration (API key, URL, etc.):
|
|
162
|
+
> ⚠️ Remember to set `ENV['YOUR_API_KEY']` or add the value to your configuration before running.
|