airb 0.0.1 → 0.1.2
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/.claude/settings.local.json +13 -0
- data/CLAUDE.md +54 -0
- data/README.md +274 -16
- data/Rakefile +1 -1
- data/exe/airb +13 -1
- data/lib/airb/organism.rb +54 -0
- data/lib/airb/ports/chat_tty.rb +79 -0
- data/lib/airb/systems/coordination.rb +9 -0
- data/lib/airb/systems/governance.rb +87 -0
- data/lib/airb/systems/identity.rb +11 -0
- data/lib/airb/systems/intelligence.rb +16 -0
- data/lib/airb/systems/monitoring.rb +9 -0
- data/lib/airb/tools/fs/edit_file.rb +42 -0
- data/lib/airb/tools/fs/list_files.rb +27 -0
- data/lib/airb/tools/fs/read_file.rb +22 -0
- data/lib/airb/version.rb +1 -1
- data/lib/airb.rb +26 -3
- data/llms.txt +306 -0
- metadata +65 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6265896136e1ee23d92dacfc00bd7245a1256af1bb20b1a4a76af33af46dfd02
|
4
|
+
data.tar.gz: 67f21a5fb4456a45f3b7c22bcb1fdd429ec84fe0e4234b4cee931b1a5b857118
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4758d0cae83f02b112640a096225b1ecd1722419ddcfbbef96509b4698343e0366ad7ab44a084f375e4ebfcb4930d234ad68d9568c560e47f946c61562171462
|
7
|
+
data.tar.gz: 1bbf93a6fbc3a6d94b4fc8f5105df79173fe0dbf699af12f27d244e1a06cf9156250da175c9287ac3c6f291b94217800d28ef6a3d1b9f8f20472ab79194a5efd
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
This is a Ruby gem called "airb" - an AI-powered programming assistant that runs in a terminal chat interface. It uses the VSM (Viable System Model) architecture to coordinate between different systems including intelligence, governance, coordination, identity, and monitoring.
|
8
|
+
|
9
|
+
## Development Commands
|
10
|
+
|
11
|
+
- `bin/setup` - Install dependencies
|
12
|
+
- `rake spec` - Run tests
|
13
|
+
- `bin/console` - Interactive console for experimentation
|
14
|
+
- `bundle exec rake install` - Install gem locally
|
15
|
+
- `rake` or `rake default` - Run both tests and RuboCop
|
16
|
+
- `rake rubocop` - Run RuboCop linter
|
17
|
+
- `exe/airb` - Run the airb CLI directly
|
18
|
+
|
19
|
+
## Architecture
|
20
|
+
|
21
|
+
The codebase follows a modular VSM-based architecture:
|
22
|
+
|
23
|
+
### Core Components
|
24
|
+
- `Airb::CLI` - Entry point that builds the organism and starts the runtime
|
25
|
+
- `Airb::Organism` - Factory that assembles all systems and tools using VSM DSL
|
26
|
+
- `Airb::Ports::ChatTTY` - Terminal interface for user interaction
|
27
|
+
|
28
|
+
### Systems (VSM Components)
|
29
|
+
- `Intelligence` - Handles LLM interactions with support for OpenAI, Anthropic, and Gemini providers
|
30
|
+
- `Governance` - Security layer that validates file operations and prompts for risky actions
|
31
|
+
- `Coordination` - Manages async message flow and turn-taking
|
32
|
+
- `Identity` - System identification
|
33
|
+
- `Monitoring` - VSM monitoring system
|
34
|
+
|
35
|
+
### Tools
|
36
|
+
File system tools in `lib/airb/tools/fs/`:
|
37
|
+
- `ListFiles` - Directory listing
|
38
|
+
- `ReadFile` - File reading with path safety
|
39
|
+
- `EditFile` - File editing with governance approval
|
40
|
+
|
41
|
+
## Configuration
|
42
|
+
|
43
|
+
- `AIRB_PROVIDER` - LLM provider (openai, anthropic, gemini)
|
44
|
+
- `AIRB_MODEL` - Specific model to use
|
45
|
+
- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` - API keys
|
46
|
+
- `VSM_LENS=1` - Enable VSM visualization dashboard
|
47
|
+
- `VSM_LENS_PORT`, `VSM_LENS_TOKEN` - Lens configuration
|
48
|
+
|
49
|
+
## Code Style
|
50
|
+
|
51
|
+
- Uses RuboCop with double quotes for strings
|
52
|
+
- Ruby 3.4+ required
|
53
|
+
- Frozen string literals enabled throughout
|
54
|
+
- VSM framework patterns for system composition
|
data/README.md
CHANGED
@@ -1,39 +1,297 @@
|
|
1
|
-
#
|
1
|
+
# airb
|
2
2
|
|
3
|
-
|
3
|
+
An open-source, CLI-based programming agent for Rubyists.
|
4
4
|
|
5
|
-
|
5
|
+
## 🌌 Why use airb?
|
6
6
|
|
7
|
-
|
7
|
+
airb is an open‑source, CLI‑based programming agent for Rubyists. We built it to explore a clean, composable agent architecture grounded in cybernetics—specifically Stafford Beer's Viable System Model (VSM)—and to make a practical tool you can run in your terminal to read, list, and edit files with the help of modern LLMs.
|
8
8
|
|
9
|
-
|
9
|
+
**In short:**
|
10
10
|
|
11
|
-
|
11
|
+
- **A new spine for agents:** Operations, Coordination, Intelligence, Governance, Identity—recursive, inspectable, testable.
|
12
|
+
- **A minimal, useful CLI** that streams responses and uses structured tool calls (no fragile "JSON from text" parsing).
|
13
|
+
- **A foundation to learn from and extend:** add tools, swap models (OpenAI/Anthropic/Gemini), plug in UI/observability, grow sub‑agents.
|
14
|
+
|
15
|
+
If you like small objects, clear seams, and UNIXy ergonomics, airb is for you.
|
16
|
+
|
17
|
+
## 🌌 Who benefits from airb?
|
18
|
+
|
19
|
+
- **Ruby developers** who want a capable, hackable terminal agent that can actually work on a codebase.
|
20
|
+
- **Framework/tool authors** exploring agent design (capsules, tools, sub‑agents) with high cohesion & low coupling.
|
21
|
+
- **Educators and teams** who need a clear, auditable loop to reason about tool calling, streaming, and safety.
|
22
|
+
- **Researchers** playing with agent recursion (sub‑agents, tool‑as‑capsule), budget homeostasis, and observability.
|
23
|
+
|
24
|
+
## 🌌 What does airb do?
|
25
|
+
|
26
|
+
A CLI programming agent that:
|
27
|
+
|
28
|
+
- **Streams assistant output** to your terminal as it thinks.
|
29
|
+
|
30
|
+
- **Uses native, structured tool calling** across providers:
|
31
|
+
- OpenAI "tools" (functions) — streaming + parallel tool calls
|
32
|
+
- Anthropic tool_use / tool_result — streaming (input_json_delta)
|
33
|
+
- Gemini function calling — non‑streaming MVP (streaming later)
|
34
|
+
|
35
|
+
- **Ships with core programming tools** (as capsules):
|
36
|
+
- `list_files(path?)` — directory listing (dirs end with /)
|
37
|
+
- `read_file(path)` — read UTF‑8 text files
|
38
|
+
- `edit_file(path, old_str, new_str)` — replace/create with confirmation
|
39
|
+
|
40
|
+
- **Runs on a VSM engine** (via the vsm gem):
|
41
|
+
- Operations — dispatches tool calls; concurrency per tool
|
42
|
+
- Coordination — session floor control & turn lifecycle
|
43
|
+
- Intelligence — LLM driver + conversation + streaming
|
44
|
+
- Governance — workspace sandbox, confirmations, budgets
|
45
|
+
- Identity — purpose/invariants & escalation hooks
|
46
|
+
|
47
|
+
- **Provides observability from day one:**
|
48
|
+
- JSONL event ledger (`.vsm.log.jsonl`)
|
49
|
+
- Optional web Lens (local SSE app) for live timelines
|
50
|
+
|
51
|
+
### High‑level architecture
|
52
|
+
|
53
|
+
```
|
54
|
+
airb (top capsule)
|
55
|
+
├─ Identity – name, invariants
|
56
|
+
├─ Governance – sandbox, confirms, budgets
|
57
|
+
├─ Coordination – floor control, turn end
|
58
|
+
├─ Intelligence – driver(OpenAI/Anthropic/Gemini), streaming, tool loop
|
59
|
+
├─ Operations – dispatch tools as child capsules (parallel)
|
60
|
+
│ ├─ list_files (tool capsule)
|
61
|
+
│ ├─ read_file (tool capsule)
|
62
|
+
│ └─ edit_file (tool capsule)
|
63
|
+
└─ Ports – ChatTTY (CLI), Lens (web)
|
64
|
+
```
|
65
|
+
|
66
|
+
## 🌌 How to use it
|
67
|
+
|
68
|
+
### Install
|
69
|
+
|
70
|
+
Requires Ruby 3.4+.
|
71
|
+
|
72
|
+
Add to your app or install globally:
|
12
73
|
|
13
74
|
```bash
|
14
|
-
|
75
|
+
# Using Bundler in a project
|
76
|
+
bundle add airb
|
77
|
+
|
78
|
+
# Or install gem globally
|
79
|
+
gem install airb
|
80
|
+
```
|
81
|
+
|
82
|
+
airb depends on the vsm gem (the agent runtime & drivers).
|
83
|
+
|
84
|
+
### Configure a provider
|
85
|
+
|
86
|
+
Pick one provider and set env vars:
|
87
|
+
|
88
|
+
```bash
|
89
|
+
# OpenAI (streaming + tools)
|
90
|
+
export AIRB_PROVIDER=openai
|
91
|
+
export OPENAI_API_KEY=sk-...
|
92
|
+
export AIRB_MODEL=gpt-5-nano # default if unset
|
93
|
+
|
94
|
+
# Anthropic (streaming + tool_use)
|
95
|
+
# export AIRB_PROVIDER=anthropic
|
96
|
+
# export ANTHROPIC_API_KEY=...
|
97
|
+
# export AIRB_MODEL=claude-sonnet-4-0 # default if unset
|
98
|
+
|
99
|
+
# Gemini (MVP: non-streaming tool calls)
|
100
|
+
# export AIRB_PROVIDER=gemini
|
101
|
+
# export GEMINI_API_KEY=...
|
102
|
+
# export AIRB_MODEL=gemini-2.5-flash # default if unset
|
103
|
+
```
|
104
|
+
|
105
|
+
### Quickstart
|
106
|
+
|
107
|
+
From the root of a Git repo:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
airb
|
111
|
+
```
|
112
|
+
|
113
|
+
Sample session:
|
114
|
+
|
115
|
+
```
|
116
|
+
airb — chat (Ctrl-C to exit)
|
117
|
+
You: what's in this directory?
|
118
|
+
<streams…>
|
119
|
+
airb: README.md
|
120
|
+
lib/
|
121
|
+
spec/
|
122
|
+
tmp/
|
123
|
+
You: open README.md
|
124
|
+
<streams…>
|
125
|
+
airb: (prints file contents)
|
126
|
+
You: replace the title with "Airb Demo"
|
127
|
+
<streams…>
|
128
|
+
confirm? Write to README.md? [y/N] y
|
129
|
+
<streams…>
|
130
|
+
airb: OK. Title updated.
|
15
131
|
```
|
16
132
|
|
17
|
-
|
133
|
+
### Live visualizer (optional)
|
134
|
+
|
135
|
+
Start the local Lens web app (SSE):
|
18
136
|
|
19
137
|
```bash
|
20
|
-
|
138
|
+
VSM_LENS=1 airb
|
139
|
+
# Lens: http://127.0.0.1:9292
|
140
|
+
```
|
141
|
+
|
142
|
+
See live timeline & sessions: user messages, assistant deltas, tool calls/results, confirms, audits.
|
143
|
+
|
144
|
+
### Configuration reference
|
145
|
+
|
146
|
+
| Variable | Meaning | Default |
|
147
|
+
|----------|---------|---------|
|
148
|
+
| `AIRB_PROVIDER` | openai \| anthropic \| gemini | openai |
|
149
|
+
| `AIRB_MODEL` | Model name for chosen provider | see examples above |
|
150
|
+
| `OPENAI_API_KEY` | OpenAI auth | — |
|
151
|
+
| `ANTHROPIC_API_KEY` | Anthropic auth | — |
|
152
|
+
| `GEMINI_API_KEY` | Gemini auth | — |
|
153
|
+
| `VSM_LENS` | 1 to enable web Lens | off |
|
154
|
+
| `VSM_LENS_PORT` | Lens port | 9292 |
|
155
|
+
| `VSM_LENS_TOKEN` | Optional access token (append ?token=...) | none |
|
156
|
+
|
157
|
+
**Workspace:** airb auto‑detects repo root (git rev-parse). If not a repo, it uses `Dir.pwd`.
|
158
|
+
|
159
|
+
### What happens on each turn?
|
160
|
+
|
161
|
+
1. You type text.
|
162
|
+
2. Intelligence appends it to the conversation and calls the provider driver.
|
163
|
+
3. The driver streams assistant text (assistant_delta).
|
164
|
+
4. If the model needs a tool, the driver emits tool_calls → Operations routes to the proper capsule.
|
165
|
+
5. The tool runs (in parallel, if multiple) and returns tool_result which is fed back to Intelligence.
|
166
|
+
6. The model produces a final assistant message; Coordination marks the turn complete.
|
167
|
+
7. Everything is emitted on the bus and logged; the Lens renders it live.
|
168
|
+
|
169
|
+
### Provider behavior (at a glance)
|
170
|
+
|
171
|
+
| Capability | OpenAI | Anthropic | Gemini (MVP) |
|
172
|
+
|------------|--------|-----------|--------------|
|
173
|
+
| Streaming text | ✅ SSE | ✅ SSE (text_delta) | ➖ (planned) |
|
174
|
+
| Structured tool calls | ✅ tools/tool_calls | ✅ tool_use/tool_result | ✅ functionCall/Response |
|
175
|
+
| Parallel tool calls | ✅ supported | ✅ supported | ✅ supported |
|
176
|
+
| System prompt handling | in messages | header param (system) | in content / safety opts |
|
177
|
+
|
178
|
+
airb normalizes these differences so your CLI experience is the same.
|
179
|
+
|
180
|
+
## Advanced Usage
|
181
|
+
|
182
|
+
### Add your own tool (as a capsule)
|
183
|
+
|
184
|
+
Create a class that inherits `VSM::ToolCapsule`, describe its schema, implement `#run`.
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
# lib/airb/tools/search_repo.rb
|
188
|
+
class SearchRepo < VSM::ToolCapsule
|
189
|
+
tool_name "search_repo"
|
190
|
+
tool_description "Search files for a regex under optional path"
|
191
|
+
tool_schema({
|
192
|
+
type: "object",
|
193
|
+
properties: { path: {type:"string"}, pattern:{type:"string"} },
|
194
|
+
required: ["pattern"]
|
195
|
+
})
|
196
|
+
|
197
|
+
# Optional: choose how it executes (fiber/thread/ractor/subprocess)
|
198
|
+
def execution_mode = :thread
|
199
|
+
|
200
|
+
def run(args)
|
201
|
+
root = governance.send(:safe_path, args["path"] || ".")
|
202
|
+
rx = Regexp.new(args["pattern"])
|
203
|
+
matches = Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).
|
204
|
+
select { |p| File.file?(p) }.
|
205
|
+
filter_map do |file|
|
206
|
+
lines = File.readlines(file, chomp:true, encoding:"UTF-8") rescue []
|
207
|
+
hits = lines.each_with_index.filter_map { |line,i| "#{file}:#{i+1}:#{line}" if rx.match?(line) }
|
208
|
+
hits unless hits.empty?
|
209
|
+
end
|
210
|
+
matches.flatten.join("\n")
|
211
|
+
end
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
Register it in your organism under Operations:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
operations do
|
219
|
+
capsule :search_repo, klass: SearchRepo
|
220
|
+
end
|
21
221
|
```
|
22
222
|
|
23
|
-
|
223
|
+
The Intelligence system automatically advertises it to the model as a structured tool (OpenAI/Anthropic/Gemini shapes).
|
224
|
+
|
225
|
+
### Create a sub‑agent (recursive capsule)
|
226
|
+
|
227
|
+
When a "tool" needs multiple steps (plan → read → patch → verify), make it a full capsule with its own 5 systems (Operations/Coordination/Intelligence/Governance/Identity). Expose it as a tool by including `VSM::ActsAsTool` and implementing `#run(args)` that orchestrates internally, then returns a summary.
|
228
|
+
|
229
|
+
This keeps the parent simple while the sub‑agent stays cohesive and testable.
|
230
|
+
|
231
|
+
### Concurrency & performance
|
24
232
|
|
25
|
-
|
233
|
+
- The runtime uses async fibers for orchestration and streaming.
|
234
|
+
- Each tool call runs in its own task; set `execution_mode` to `:thread` for CPU‑heavier work.
|
235
|
+
- Governance can add timeouts and semaphores to limit concurrent tool calls.
|
26
236
|
|
27
|
-
|
237
|
+
### Safety & governance
|
28
238
|
|
29
|
-
|
239
|
+
- airb runs in a workspace sandbox (repo root or CWD).
|
240
|
+
- `edit_file` prompts for confirmation before writing.
|
241
|
+
- You can extend Governance to show diffs, enforce allowlists, or budget tokens/time.
|
30
242
|
|
31
|
-
|
243
|
+
## Troubleshooting
|
244
|
+
|
245
|
+
- **"No streaming"** — Gemini driver is MVP (non‑streaming). Use OpenAI/Anthropic for streaming.
|
246
|
+
- **"Path escapes workspace"** — Governance blocked a write; run airb from the repo root or adjust logic.
|
247
|
+
- **"No tool calls"** — Ensure your provider key & model are set; some models require enabling tools.
|
248
|
+
- **Lens shows nothing** — Start with `VSM_LENS=1`, then open http://127.0.0.1:9292.
|
249
|
+
|
250
|
+
## Table of Contents
|
251
|
+
|
252
|
+
- [Why use airb?](#-why-use-airb)
|
253
|
+
- [Who benefits from airb?](#-who-benefits-from-airb)
|
254
|
+
- [What does airb do?](#-what-does-airb-do)
|
255
|
+
- [How to use it](#-how-to-use-it)
|
256
|
+
- [Install](#install)
|
257
|
+
- [Configure a provider](#configure-a-provider)
|
258
|
+
- [Quickstart](#quickstart)
|
259
|
+
- [Live visualizer](#live-visualizer-optional)
|
260
|
+
- [Configuration reference](#configuration-reference)
|
261
|
+
- [Provider behavior](#provider-behavior-at-a-glance)
|
262
|
+
- [Advanced Usage](#advanced-usage)
|
263
|
+
- [Add your own tool](#add-your-own-tool-as-a-capsule)
|
264
|
+
- [Create a sub-agent](#create-a-sub-agent-recursive-capsule)
|
265
|
+
- [Concurrency & performance](#concurrency--performance)
|
266
|
+
- [Safety & governance](#safety--governance)
|
267
|
+
- [Troubleshooting](#troubleshooting)
|
268
|
+
- [Roadmap](#roadmap)
|
269
|
+
- [Contributing](#contributing)
|
270
|
+
- [License](#license)
|
271
|
+
- [Acknowledgements](#acknowledgements)
|
272
|
+
|
273
|
+
## Roadmap
|
274
|
+
|
275
|
+
- Streaming for Gemini driver
|
276
|
+
- Diff previews & undo for `edit_file`
|
277
|
+
- MCP client/server ports (tool ecosystem)
|
278
|
+
- "Command mode" (`airb -e "…"`) for one‑shot automation
|
279
|
+
- Rich Lens (search, replay, swimlanes, token counters)
|
280
|
+
- Additional built‑in capsules (planner, tester, editor)
|
32
281
|
|
33
282
|
## Contributing
|
34
283
|
|
35
|
-
Bug reports and
|
284
|
+
Bug reports, ideas, and PRs welcome!
|
285
|
+
|
286
|
+
- Please keep code SRP‑friendly, name things clearly, and favor composition over inheritance.
|
287
|
+
- Tests for drivers should include small fixture streams → expected events.
|
36
288
|
|
37
289
|
## License
|
38
290
|
|
39
|
-
|
291
|
+
MIT (same as vsm), unless noted otherwise in subdirectories.
|
292
|
+
|
293
|
+
## Acknowledgements
|
294
|
+
|
295
|
+
- Inspired by Stafford Beer's Viable System Model and the broader cybernetics community.
|
296
|
+
- Thanks to the Ruby OSS ecosystem for gems like async that make structured concurrency practical.
|
297
|
+
- Early discussions about good agent loops, tool use, and safety shaped this project.
|
data/Rakefile
CHANGED
data/exe/airb
CHANGED
@@ -1,3 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
|
4
|
+
# --- Keep this CLI independent of any project's Bundler context ----------------
|
5
|
+
# If someone runs `airb` inside a Bundler-managed project, remove Bundler hooks
|
6
|
+
# so we resolve *this gem’s* dependencies, not the app’s Gemfile.
|
7
|
+
ENV.delete('BUNDLE_GEMFILE')
|
8
|
+
ENV.delete('BUNDLE_BIN_PATH')
|
9
|
+
if (rubyopt = ENV['RUBYOPT'])
|
10
|
+
ENV['RUBYOPT'] = rubyopt.split.reject { |x| x.include?('bundler/setup') }.join(' ')
|
11
|
+
end
|
12
|
+
ENV.delete('RUBYGEMS_GEMDEPS') # avoid implicit Gemfile activation
|
13
|
+
|
14
|
+
require 'airb'
|
15
|
+
Airb::CLI.start
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "systems/intelligence"
|
3
|
+
require_relative "systems/governance"
|
4
|
+
require_relative "systems/coordination"
|
5
|
+
require_relative "systems/identity"
|
6
|
+
require_relative "systems/monitoring"
|
7
|
+
require_relative "tools/fs/list_files"
|
8
|
+
require_relative "tools/fs/read_file"
|
9
|
+
require_relative "tools/fs/edit_file"
|
10
|
+
|
11
|
+
module Airb
|
12
|
+
module Organism
|
13
|
+
def self.build
|
14
|
+
workspace_root = `git rev-parse --show-toplevel`.strip
|
15
|
+
workspace_root = Dir.pwd if workspace_root.empty?
|
16
|
+
|
17
|
+
provider = (ENV["AIRB_PROVIDER"] || "openai").downcase
|
18
|
+
|
19
|
+
driver =
|
20
|
+
case provider
|
21
|
+
when "anthropic"
|
22
|
+
VSM::Drivers::Anthropic::AsyncDriver.new(
|
23
|
+
api_key: ENV.fetch("ANTHROPIC_API_KEY"),
|
24
|
+
model: ENV["AIRB_MODEL"] || "claude-sonnet-4-0"
|
25
|
+
)
|
26
|
+
when "gemini"
|
27
|
+
VSM::Drivers::Gemini::AsyncDriver.new(
|
28
|
+
api_key: ENV.fetch("GEMINI_API_KEY"),
|
29
|
+
model: ENV["AIRB_MODEL"] || "gemini-2.5-flash"
|
30
|
+
)
|
31
|
+
else
|
32
|
+
VSM::Drivers::OpenAI::AsyncDriver.new(
|
33
|
+
api_key: ENV.fetch("OPENAI_API_KEY"),
|
34
|
+
model: ENV["AIRB_MODEL"] || "gpt-5-nano"
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
VSM::DSL.define(:airb) do
|
39
|
+
identity klass: Airb::Systems::Identity, args: { name: "airb", invariants: [] }
|
40
|
+
governance klass: Airb::Systems::Governance, args: { workspace_root: workspace_root }
|
41
|
+
coordination klass: Airb::Systems::Coordination
|
42
|
+
intelligence klass: Airb::Systems::Intelligence, args: { driver: driver }
|
43
|
+
monitoring klass: VSM::Monitoring
|
44
|
+
|
45
|
+
operations do
|
46
|
+
capsule :list_files, klass: Airb::Tools::FS::ListFiles
|
47
|
+
capsule :read_file, klass: Airb::Tools::FS::ReadFile
|
48
|
+
capsule :edit_file, klass: Airb::Tools::FS::EditFile
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
require "securerandom"
|
4
|
+
module Airb
|
5
|
+
module Ports
|
6
|
+
class ChatTTY < VSM::Port
|
7
|
+
def should_render?(message)
|
8
|
+
[:assistant_delta, :assistant, :tool_call, :tool_result, :confirm_request].include?(message.kind)
|
9
|
+
end
|
10
|
+
|
11
|
+
def loop
|
12
|
+
session_id = SecureRandom.uuid
|
13
|
+
@capsule.roles[:coordination].grant_floor!(session_id)
|
14
|
+
@streaming_active = false
|
15
|
+
display_banner
|
16
|
+
print "\e[94mYou\e[0m: "
|
17
|
+
|
18
|
+
while (line = $stdin.gets&.chomp)
|
19
|
+
@capsule.bus.emit VSM::Message.new(kind: :user, payload: line, meta: { session_id: session_id }, path: [:airb])
|
20
|
+
@capsule.roles[:coordination].wait_for_turn_end(session_id)
|
21
|
+
print "\e[94mYou\e[0m: "
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def display_banner
|
28
|
+
puts <<~BANNER
|
29
|
+
\e[91m
|
30
|
+
██████ ██ ██████ ██████
|
31
|
+
██ ██ ██ ██ ██ ██ ██
|
32
|
+
████████ ██ ██████ ██████
|
33
|
+
██ ██ ██ ██ ██ ██ ██
|
34
|
+
██ ██ ██ ██ ██ ██████
|
35
|
+
\e[0m
|
36
|
+
\e[96mAI-powered Ruby assistant\e[0m (Ctrl-C to exit)
|
37
|
+
BANNER
|
38
|
+
end
|
39
|
+
|
40
|
+
def render_out(message)
|
41
|
+
case message.kind
|
42
|
+
when :assistant_delta
|
43
|
+
@streaming_active = true
|
44
|
+
$stdout.print(message.payload)
|
45
|
+
$stdout.flush
|
46
|
+
when :assistant
|
47
|
+
if @streaming_active
|
48
|
+
# We already streamed the content; just end the line cleanly.
|
49
|
+
puts
|
50
|
+
else
|
51
|
+
puts
|
52
|
+
puts "\e[93mairb\e[0m: #{message.payload}"
|
53
|
+
end
|
54
|
+
@streaming_active = false
|
55
|
+
# Prompt is printed by the input loop after turn end
|
56
|
+
when :tool_call
|
57
|
+
tool = message.payload[:tool]
|
58
|
+
puts
|
59
|
+
puts "\e[90m→ tool\e[0m #{tool}"
|
60
|
+
when :tool_result
|
61
|
+
# Suppress tool result output; we only announce that the tool was called.
|
62
|
+
# Intentionally no-op for cleaner UI.
|
63
|
+
when :confirm_request
|
64
|
+
print "\n\e[95mconfirm?\e[0m #{message.payload} [y/N] "
|
65
|
+
ans = ($stdin.gets || "").strip.downcase.start_with?("y")
|
66
|
+
@capsule.bus.emit VSM::Message.new(
|
67
|
+
kind: :confirm_response,
|
68
|
+
payload: { accepted: ans },
|
69
|
+
meta: message.meta, # preserve corr_id/session_id
|
70
|
+
path: [:airb, :governance]
|
71
|
+
)
|
72
|
+
when :audit, :policy, :progress
|
73
|
+
# optional: print to stderr or keep quiet for now
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Airb
|
3
|
+
module Systems
|
4
|
+
class Governance < VSM::Governance
|
5
|
+
def initialize(workspace_root:)
|
6
|
+
@workspace_root = File.expand_path(workspace_root)
|
7
|
+
@pending = {} # corr_id => original :tool_call message
|
8
|
+
end
|
9
|
+
|
10
|
+
# Capture bus reference for emitting confirm requests, etc.
|
11
|
+
def observe(bus) = (@bus = bus)
|
12
|
+
|
13
|
+
def enforce(message, &pass)
|
14
|
+
case message.kind
|
15
|
+
when :tool_call
|
16
|
+
if risky?(message)
|
17
|
+
ensure_corr_id!(message)
|
18
|
+
@pending[message.corr_id] = message
|
19
|
+
@bus.emit VSM::Message.new(
|
20
|
+
kind: :confirm_request,
|
21
|
+
payload: confirm_text(message),
|
22
|
+
meta: message.meta.merge({ corr_id: message.corr_id }),
|
23
|
+
path: [:airb, :governance]
|
24
|
+
)
|
25
|
+
return true # swallow for now; will resume on confirm_response
|
26
|
+
else
|
27
|
+
check_paths!(message)
|
28
|
+
end
|
29
|
+
when :confirm_response
|
30
|
+
corr = message.meta&.fetch(:corr_id, nil)
|
31
|
+
if corr && (orig = @pending.delete(corr))
|
32
|
+
if message.payload[:accepted]
|
33
|
+
# proceed with original tool_call
|
34
|
+
return pass.call(orig)
|
35
|
+
else
|
36
|
+
# inform user and drop
|
37
|
+
@bus.emit VSM::Message.new(
|
38
|
+
kind: :assistant,
|
39
|
+
payload: "Cancelled.",
|
40
|
+
meta: orig.meta,
|
41
|
+
path: [:airb, :governance]
|
42
|
+
)
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
pass.call(message)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def risky?(message)
|
54
|
+
message.payload[:tool].to_s == "edit_file"
|
55
|
+
end
|
56
|
+
|
57
|
+
def confirm_text(message)
|
58
|
+
args = message.payload[:args] || {}
|
59
|
+
path = args["path"] || "(no path)"
|
60
|
+
"Write to #{path}? (shows diff in a future version)"
|
61
|
+
end
|
62
|
+
|
63
|
+
def ensure_corr_id!(message)
|
64
|
+
message.corr_id ||= SecureRandom.uuid
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_paths!(message)
|
68
|
+
args = message.payload[:args] || {}
|
69
|
+
if message.payload[:tool].to_s == "edit_file"
|
70
|
+
safe_path(args.fetch("path"))
|
71
|
+
elsif message.payload[:tool].to_s == "read_file"
|
72
|
+
safe_path(args.fetch("path"))
|
73
|
+
elsif message.payload[:tool].to_s == "list_files"
|
74
|
+
path = args["path"]
|
75
|
+
safe_path(path) if path && !path.empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def safe_path(rel)
|
80
|
+
full = File.expand_path(File.join(@workspace_root, rel.to_s))
|
81
|
+
raise "Path escapes workspace" unless full.start_with?(@workspace_root)
|
82
|
+
full
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Airb
|
3
|
+
module Systems
|
4
|
+
class Intelligence < VSM::Intelligence
|
5
|
+
SYSTEM_PROMPT = <<~PROMPT
|
6
|
+
You are "airb", a careful coding assistant inside a git workspace.
|
7
|
+
Use tools when needed. Prefer minimal, reversible edits and concise explanations.
|
8
|
+
PROMPT
|
9
|
+
|
10
|
+
def initialize(driver:)
|
11
|
+
super(driver: driver, system_prompt: SYSTEM_PROMPT)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "fileutils"
|
3
|
+
module Airb
|
4
|
+
module Tools
|
5
|
+
module FS
|
6
|
+
class EditFile < VSM::ToolCapsule
|
7
|
+
tool_name "edit_file"
|
8
|
+
tool_description "Replace old_str with new_str in file (create if old_str is empty). Returns 'OK'."
|
9
|
+
tool_schema({
|
10
|
+
type: "object",
|
11
|
+
properties: {
|
12
|
+
path: { type: "string" },
|
13
|
+
old_str: { type: "string" },
|
14
|
+
new_str: { type: "string" }
|
15
|
+
},
|
16
|
+
required: ["path","old_str","new_str"]
|
17
|
+
})
|
18
|
+
|
19
|
+
def execution_mode = :fiber # change to :thread if you do heavy CPU work
|
20
|
+
|
21
|
+
def run(args)
|
22
|
+
path = governance.send(:safe_path, args.fetch("path"))
|
23
|
+
old = args.fetch("old_str")
|
24
|
+
newv = args.fetch("new_str")
|
25
|
+
|
26
|
+
if !File.exist?(path) && old.to_s.empty?
|
27
|
+
FileUtils.mkdir_p(File.dirname(path))
|
28
|
+
File.write(path, newv)
|
29
|
+
return "OK"
|
30
|
+
end
|
31
|
+
|
32
|
+
content = File.read(path, mode: "r:UTF-8")
|
33
|
+
replaced = content.gsub(old, newv)
|
34
|
+
raise "old_str not found" if replaced == content && !old.empty?
|
35
|
+
File.write(path, replaced)
|
36
|
+
"OK"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Airb
|
3
|
+
module Tools
|
4
|
+
module FS
|
5
|
+
class ListFiles < VSM::ToolCapsule
|
6
|
+
tool_name "list_files"
|
7
|
+
tool_description "List files/directories under a path (default: .). Directories end with '/'."
|
8
|
+
tool_schema({
|
9
|
+
type: "object",
|
10
|
+
properties: { path: { type: "string" } },
|
11
|
+
required: []
|
12
|
+
})
|
13
|
+
|
14
|
+
def run(args)
|
15
|
+
path = args["path"].to_s.empty? ? "." : args["path"]
|
16
|
+
root = governance.send(:safe_path, path) rescue Dir.pwd
|
17
|
+
entries = Dir.children(root).sort.map do |e|
|
18
|
+
full = File.join(root, e)
|
19
|
+
File.directory?(full) ? "#{e}/" : e
|
20
|
+
end
|
21
|
+
entries.join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Airb
|
3
|
+
module Tools
|
4
|
+
module FS
|
5
|
+
class ReadFile < VSM::ToolCapsule
|
6
|
+
tool_name "read_file"
|
7
|
+
tool_description "Read a UTF-8 text file at relative path."
|
8
|
+
tool_schema({
|
9
|
+
type: "object",
|
10
|
+
properties: { path: { type: "string" } },
|
11
|
+
required: ["path"]
|
12
|
+
})
|
13
|
+
|
14
|
+
def run(args)
|
15
|
+
path = governance.send(:safe_path, args.fetch("path"))
|
16
|
+
File.read(path, mode: "r:UTF-8")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
data/lib/airb/version.rb
CHANGED
data/lib/airb.rb
CHANGED
@@ -1,8 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "vsm"
|
4
|
+
require_relative "airb/organism"
|
5
|
+
require_relative "airb/ports/chat_tty"
|
4
6
|
|
5
7
|
module Airb
|
6
|
-
class
|
7
|
-
|
8
|
+
class CLI
|
9
|
+
def self.start
|
10
|
+
$stdout.sync = true
|
11
|
+
$stderr.sync = true
|
12
|
+
capsule = Airb::Organism.build
|
13
|
+
hub = nil
|
14
|
+
|
15
|
+
# Optional: live visualizer (Lens) from VSM
|
16
|
+
if ENV["VSM_LENS"] == "1"
|
17
|
+
hub = VSM::Lens.attach!(
|
18
|
+
capsule,
|
19
|
+
host: "127.0.0.1",
|
20
|
+
port: (ENV["VSM_LENS_PORT"] || 9292).to_i,
|
21
|
+
token: ENV["VSM_LENS_TOKEN"]
|
22
|
+
)
|
23
|
+
puts "Lens: http://127.0.0.1:#{ENV['VSM_LENS_PORT'] || 9292}"
|
24
|
+
end
|
25
|
+
|
26
|
+
port = Airb::Ports::ChatTTY.new(capsule:)
|
27
|
+
VSM::Runtime.start(capsule, ports: [port]) # async reactor + port loop
|
28
|
+
end
|
29
|
+
end
|
8
30
|
end
|
31
|
+
|
data/llms.txt
ADDED
@@ -0,0 +1,306 @@
|
|
1
|
+
|
2
|
+
airb / VSM — llms.txt
|
3
|
+
=====================
|
4
|
+
|
5
|
+
Purpose of this file
|
6
|
+
--------------------
|
7
|
+
Give any LLM or code-editing agent **everything it needs** to:
|
8
|
+
1) understand what `airb` is and how it works,
|
9
|
+
2) make **safe, minimal edits**,
|
10
|
+
3) add or modify tools and sub-agents (capsules),
|
11
|
+
4) run and validate the project locally,
|
12
|
+
5) keep behavior consistent across OpenAI / Anthropic / Gemini providers,
|
13
|
+
6) use the built-in observability (Lens) to reason about live behavior.
|
14
|
+
|
15
|
+
This file summarizes architecture, invariants, directories, message semantics, configuration, and editing protocols.
|
16
|
+
|
17
|
+
|
18
|
+
High-level mission
|
19
|
+
------------------
|
20
|
+
- **Why**: Explore a clean, recursive architecture for AI agents grounded in cybernetics (Stafford Beer’s Viable System Model), and ship a practical **open-source CLI programming agent** for Rubyists.
|
21
|
+
- **Who**: Ruby developers, agent researchers, educators, and tool authors.
|
22
|
+
- **What**: A terminal chatbot that streams answers and uses **structured tool calls** to list/read/edit files in a workspace. Built on the `vsm` runtime.
|
23
|
+
|
24
|
+
|
25
|
+
Project at a glance
|
26
|
+
-------------------
|
27
|
+
- Language: Ruby 3.4+
|
28
|
+
- Runtime: `vsm` gem (async reactor, Capsules, VSM systems, tools-as-capsules)
|
29
|
+
- Providers (from `vsm`): OpenAI (`AsyncDriver`), Anthropic (`AsyncDriver`), Gemini (`AsyncDriver`)
|
30
|
+
- Core tools (capsules): `list_files`, `read_file`, `edit_file`
|
31
|
+
- Observability: JSONL ledger + optional **Lens** (local SSE web app)
|
32
|
+
- Terminal UI: `ChatTTY` (streams deltas, handles confirmations)
|
33
|
+
- Concurrency: async fibers; tools choose execution mode (`:fiber` / `:thread`)
|
34
|
+
|
35
|
+
|
36
|
+
Directory layout (airb)
|
37
|
+
-----------------------
|
38
|
+
```
|
39
|
+
exe/airb # executable
|
40
|
+
lib/airb.rb # CLI entry; starts capsule + ports + optional Lens
|
41
|
+
lib/airb/organism.rb # builds the top-level capsule via VSM::DSL
|
42
|
+
|
43
|
+
lib/airb/ports/chat_tty.rb # terminal port (streaming, confirmations)
|
44
|
+
|
45
|
+
lib/airb/systems/coordination.rb
|
46
|
+
lib/airb/systems/governance.rb
|
47
|
+
lib/airb/systems/identity.rb
|
48
|
+
lib/airb/systems/intelligence.rb
|
49
|
+
lib/airb/systems/monitoring.rb # thin wrapper over VSM::Monitoring
|
50
|
+
|
51
|
+
lib/airb/tools/fs/list_files.rb
|
52
|
+
lib/airb/tools/fs/read_file.rb
|
53
|
+
lib/airb/tools/fs/edit_file.rb
|
54
|
+
|
55
|
+
spec/integration_spec.rb # example integration (fake driver)
|
56
|
+
```
|
57
|
+
|
58
|
+
Do not remove these files; they define the public behavior expected by users and tests.
|
59
|
+
|
60
|
+
|
61
|
+
Core concepts (VSM + Capsules)
|
62
|
+
------------------------------
|
63
|
+
- **Capsule**: a self-contained agent unit with 5 named systems (roles):
|
64
|
+
- **Identity**: name/invariants; escalation hooks.
|
65
|
+
- **Governance**: **safety** (sandbox, confirms, budgets, policies).
|
66
|
+
- **Coordination**: **scheduling/floor** control, turn lifecycle.
|
67
|
+
- **Intelligence**: connects to LLM driver, manages conversation, streams output, triggers tools.
|
68
|
+
- **Operations**: dispatches **tools** (child capsules); parallel execution.
|
69
|
+
|
70
|
+
- **Tools as Capsules**:
|
71
|
+
- Implement class inheriting `VSM::ToolCapsule`.
|
72
|
+
- Declare `tool_name`, `tool_description`, `tool_schema` (JSON Schema).
|
73
|
+
- Implement `#run(args)` and optional `#execution_mode` (`:fiber` or `:thread`).
|
74
|
+
- Tools automatically expose cross-provider descriptors via `Descriptor#to_openai_tool / #to_anthropic_tool / #to_gemini_tool`.
|
75
|
+
|
76
|
+
- **Ports**:
|
77
|
+
- External adapters (CLI, Lens). Ports subscribe to the bus and render messages.
|
78
|
+
|
79
|
+
- **Recursion**:
|
80
|
+
- Any capsule can itself contain sub-capsules (planning, editor, tester). Sub-agents can also be exposed as a **single tool** to the parent via `ActsAsTool`.
|
81
|
+
|
82
|
+
|
83
|
+
Message bus semantics
|
84
|
+
---------------------
|
85
|
+
All activity flows through the async bus as `VSM::Message` structs:
|
86
|
+
|
87
|
+
- `kind` (enum used by systems & ports):
|
88
|
+
- `:user` — user input from CLI.
|
89
|
+
- `:assistant_delta` — streaming text tokens to terminal.
|
90
|
+
- `:assistant` — final turn text.
|
91
|
+
- `:tool_call` — Intelligence wants Operations to run a tool.
|
92
|
+
- `:tool_result` — result string from a tool (routed back to Intelligence).
|
93
|
+
- `:confirm_request` / `:confirm_response` — Governance confirmation loop.
|
94
|
+
- `:policy`, `:audit`, `:progress` — optional advisories/logs.
|
95
|
+
- `meta`:
|
96
|
+
- `session_id` (required for multi-turn coherence + floor control).
|
97
|
+
- (optional) `tool`, `elapsed_ms`, `args_size`, etc.
|
98
|
+
- `corr_id`:
|
99
|
+
- Correlates `:tool_call` with its `:tool_result`.
|
100
|
+
- `path`:
|
101
|
+
- Lane like `[:airb, :operations, :read_file]` (Lens uses this for swimlanes).
|
102
|
+
|
103
|
+
**Turn lifecycle**:
|
104
|
+
1) Port emits `:user`.
|
105
|
+
2) Intelligence calls provider driver → streams `:assistant_delta` → may emit `:tool_calls`.
|
106
|
+
3) Operations runs requested tools concurrently → `:tool_result` messages.
|
107
|
+
4) Intelligence continues with results → emits final `:assistant`.
|
108
|
+
5) Coordination marks turn end (so Port can unblock and prompt again).
|
109
|
+
|
110
|
+
|
111
|
+
Provider abstraction (drivers live in vsm)
|
112
|
+
------------------------------------------
|
113
|
+
airb is provider-agnostic; drivers normalize to three events:
|
114
|
+
|
115
|
+
- `:assistant_delta` (streaming text)
|
116
|
+
- `:assistant_final` (final text)
|
117
|
+
- `:tool_calls` (array of `{ id:, name:, arguments: Hash }`)
|
118
|
+
|
119
|
+
Differences:
|
120
|
+
- **OpenAI**: messages include a `system` message; `tools` with `function` schema; SSE streaming.
|
121
|
+
- **Anthropic**: `system` provided in request header/body; `tool_use` streamed via `input_json_delta` fragments; we buffer & emit a single `:tool_calls` event per block.
|
122
|
+
- **Gemini**: `functionDeclarations` + `functionCall/functionResponse`; MVP driver is non-streaming; you can add streaming later.
|
123
|
+
|
124
|
+
|
125
|
+
Governance rules (safety)
|
126
|
+
-------------------------
|
127
|
+
- airb operates in a **workspace sandbox** (Git root or CWD). Governance validates file paths.
|
128
|
+
- `edit_file` requires **interactive confirmation** via `:confirm_request/response`.
|
129
|
+
- Future: token/time budgets, rate limits, diff previews, undo.
|
130
|
+
|
131
|
+
**LLM editing constraint**: Do not weaken path checks, confirms, or invariants without an explicit instruction in a change request.
|
132
|
+
|
133
|
+
|
134
|
+
CLI behavior (ChatTTY)
|
135
|
+
----------------------
|
136
|
+
- Reads stdin lines, emits `:user` with a unique `session_id`.
|
137
|
+
- Streams `:assistant_delta` inline; prints `:assistant` on completion.
|
138
|
+
- Prompts for confirmation on `:confirm_request`, echoes decision as `:confirm_response`.
|
139
|
+
- Hands off turn control to Coordination (waits for final `:assistant` before re-prompting).
|
140
|
+
|
141
|
+
|
142
|
+
Observability (Monitoring + Lens)
|
143
|
+
---------------------------------
|
144
|
+
- JSONL ledger: `.vsm.log.jsonl` (one line per message).
|
145
|
+
- Lens: local SSE web app (bundled in `vsm`).
|
146
|
+
- Enable with `VSM_LENS=1` when running airb.
|
147
|
+
- URL: `http://127.0.0.1:9292`
|
148
|
+
- Shows timeline, sessions, tool calls/results, lanes by `path`.
|
149
|
+
|
150
|
+
Make events useful: include `meta.session_id`, `path`, and `meta.tool` (for calls/results).
|
151
|
+
|
152
|
+
|
153
|
+
Configuration (env vars)
|
154
|
+
------------------------
|
155
|
+
```
|
156
|
+
AIRB_PROVIDER = openai | anthropic | gemini (default: openai)
|
157
|
+
AIRB_MODEL = provider-specific model name
|
158
|
+
OPENAI_API_KEY = ...
|
159
|
+
ANTHROPIC_API_KEY = ...
|
160
|
+
GEMINI_API_KEY = ...
|
161
|
+
VSM_LENS = 1 to enable web visualizer (default off)
|
162
|
+
VSM_LENS_PORT = 9292 (default)
|
163
|
+
VSM_LENS_TOKEN = optional access token for Lens (append ?token=...)
|
164
|
+
```
|
165
|
+
|
166
|
+
Workspace: if `git rev-parse --show-toplevel` fails, airb uses `Dir.pwd`.
|
167
|
+
|
168
|
+
|
169
|
+
How to add a new tool (capsule)
|
170
|
+
-------------------------------
|
171
|
+
**Goal**: minimal diff, high cohesion, governance-aware.
|
172
|
+
|
173
|
+
1) Create a file, e.g. `lib/airb/tools/search_repo.rb`:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class SearchRepo < VSM::ToolCapsule
|
177
|
+
tool_name "search_repo"
|
178
|
+
tool_description "Search files for a regex under optional path"
|
179
|
+
tool_schema({
|
180
|
+
type:"object",
|
181
|
+
properties:{ path:{type:"string"}, pattern:{type:"string"} },
|
182
|
+
required:["pattern"]
|
183
|
+
})
|
184
|
+
|
185
|
+
def execution_mode = :thread # CPU-ish scan; allow parallelism
|
186
|
+
|
187
|
+
def run(args)
|
188
|
+
root = governance.send(:safe_path, args["path"] || ".")
|
189
|
+
rx = Regexp.new(args["pattern"])
|
190
|
+
matches = Dir.glob("#{root}/**/*", File::FNM_DOTMATCH)
|
191
|
+
.select { |p| File.file?(p) }
|
192
|
+
.flat_map do |file|
|
193
|
+
lines = File.readlines(file, chomp:true, encoding:"UTF-8") rescue []
|
194
|
+
lines.each_with_index.filter_map { |line,i| "#{file}:#{i+1}:#{line}" if rx.match?(line) }
|
195
|
+
end
|
196
|
+
matches.join("\n")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
2) Register it in `lib/airb/organism.rb` under `operations`:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
operations do
|
205
|
+
capsule :search_repo, klass: SearchRepo
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
3) (Optional) Add a spec covering one happy path.
|
210
|
+
|
211
|
+
No changes required in `Intelligence`—it auto-advertises all tool descriptors to the provider driver.
|
212
|
+
|
213
|
+
|
214
|
+
How to add a sub-agent (recursive capsule)
|
215
|
+
------------------------------------------
|
216
|
+
Use this when a feature needs planning/verification steps (e.g., “Editor” agent).
|
217
|
+
|
218
|
+
1) Create a capsule with its own systems (ops/coord/intel/gov/identity) under `lib/airb/capsules/editor/...`.
|
219
|
+
2) If you want to expose it to the parent as a single tool, `include VSM::ActsAsTool` and implement `#run(args)` that orchestrates the inner loop and returns a result string.
|
220
|
+
3) Register the capsule under `operations`. The parent will see it as a tool; the Lens will show its lane via the `path` hierarchy.
|
221
|
+
|
222
|
+
|
223
|
+
Testing guidance
|
224
|
+
----------------
|
225
|
+
- **Unit**: pure Ruby classes (tools, governance checks) should be tested without any network.
|
226
|
+
- **Integration**: use a **fake driver** that emits `:tool_calls` then `:assistant_final`. See `spec/integration_spec.rb`.
|
227
|
+
- **Contract**: if you change message kinds/meta/path, update both the Port and tests.
|
228
|
+
- **Style**: run `rubocop` if present; keep methods small; be explicit with names (POODR/SOLID).
|
229
|
+
|
230
|
+
|
231
|
+
Troubleshooting
|
232
|
+
---------------
|
233
|
+
- No streaming: Gemini driver is MVP non-streaming; use OpenAI/Anthropic for streaming.
|
234
|
+
- “Path escapes workspace”: Governance blocked unsafe path; run in correct repo or update the rule intentionally.
|
235
|
+
- No tool calls: Ensure provider/model supports tools; keys set correctly.
|
236
|
+
- Lens empty: Start with `VSM_LENS=1` and open `http://127.0.0.1:9292`.
|
237
|
+
|
238
|
+
|
239
|
+
Roadmap (safe to implement)
|
240
|
+
---------------------------
|
241
|
+
- Diff preview & undo for `edit_file` confirmations.
|
242
|
+
- Concurrency limits (Async semaphores) per tool family.
|
243
|
+
- Gemini streaming endpoint / Live API support.
|
244
|
+
- MCP client/server ports (map `ActsAsTool` to MCP tool specs).
|
245
|
+
- Replay mode in Lens (read JSONL and scrub timeline).
|
246
|
+
- “Command mode”: `airb -e "message"` for one-shot runs.
|
247
|
+
|
248
|
+
|
249
|
+
LLM Edit Protocol (LEP)
|
250
|
+
-----------------------
|
251
|
+
**Use this when applying automated changes.**
|
252
|
+
|
253
|
+
1) **Understand the goal**: restate the change request; identify files to touch.
|
254
|
+
2) **Plan minimal diffs**: prefer small, reversible edits; keep public method signatures stable unless requested.
|
255
|
+
3) **Safety first**:
|
256
|
+
- Never weaken Governance sandbox/confirm flows without explicit instruction.
|
257
|
+
- Keep `session_id`, `corr_id`, `path` and message kinds consistent.
|
258
|
+
4) **Make the change**:
|
259
|
+
- Edit only necessary files.
|
260
|
+
- For new tools, add a capsule file + register in `organism.rb`.
|
261
|
+
- For provider changes, modify `organism.rb` env switch or the `vsm` drivers (not here).
|
262
|
+
5) **Update docs/tests**:
|
263
|
+
- Adjust README if behavior changes.
|
264
|
+
- Add/modify a spec (happy path).
|
265
|
+
6) **Self-check**:
|
266
|
+
- Static scan for obvious errors (names, requires).
|
267
|
+
- Ensure `airb` still runs with `OPENAI_API_KEY=...` in a repo.
|
268
|
+
7) **Commit guidance**:
|
269
|
+
- Conventional message: `feat(tool): add search_repo capsule` / `fix(intel): avoid duplicate tool_calls`.
|
270
|
+
- Include a brief rationale.
|
271
|
+
|
272
|
+
If conflicted, prefer **safer behavior** (ask for confirmation, maintain invariants) over convenience.
|
273
|
+
|
274
|
+
|
275
|
+
Quick reference: critical files
|
276
|
+
-------------------------------
|
277
|
+
- Entry: `exe/airb`, `lib/airb.rb`
|
278
|
+
- Organism wiring: `lib/airb/organism.rb`
|
279
|
+
- CLI Port: `lib/airb/ports/chat_tty.rb`
|
280
|
+
- Intelligence: `lib/airb/systems/intelligence.rb`
|
281
|
+
- Governance: `lib/airb/systems/governance.rb`
|
282
|
+
- Coordination: `lib/airb/systems/coordination.rb`
|
283
|
+
- Tools: `lib/airb/tools/**.rb`
|
284
|
+
- Tests: `spec/**`
|
285
|
+
|
286
|
+
|
287
|
+
Example prompts to drive changes (for humans)
|
288
|
+
---------------------------------------------
|
289
|
+
- “Add a tool to search the repo for `TODO` and return matches with file:line.”
|
290
|
+
- “Require confirmation for files larger than 1 MB when reading them.”
|
291
|
+
- “Expose an `editor` sub-agent capsule that plans a small patch then applies it via `edit_file`.”
|
292
|
+
|
293
|
+
|
294
|
+
Glossary
|
295
|
+
--------
|
296
|
+
- **Capsule**: a composable agent component with the five VSM systems.
|
297
|
+
- **Tool**: a capsule exposed to the LLM via a JSON schema descriptor.
|
298
|
+
- **Turn**: a user→assistant exchange, possibly with tool calls.
|
299
|
+
- **Floor**: turn ownership; Coordination keeps output ordered per session.
|
300
|
+
- **Lens**: local web visualizer (SSE) for live events.
|
301
|
+
- **Descriptor**: the object that maps a tool to provider-specific shapes.
|
302
|
+
|
303
|
+
|
304
|
+
End
|
305
|
+
---
|
306
|
+
If you (the LLM) need to modify behavior, follow **LLM Edit Protocol**, keep deltas small, and preserve the message semantics and Governance guarantees.
|
metadata
CHANGED
@@ -1,21 +1,62 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: airb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Werner
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: vsm
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.1'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.1'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.13'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.13'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: async-rspec
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.17'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.17'
|
54
|
+
description: " airb is an open-source CLI programming agent that helps developers
|
55
|
+
build software\n using modern LLMs (OpenAI, Anthropic, Gemini). Built on a clean,
|
56
|
+
composable architecture \n inspired by Stafford Beer's Viable System Model, it
|
57
|
+
features streaming responses, structured \n tool calling, built-in file operations,
|
58
|
+
and optional web-based observability. Designed for \n hackability with small
|
59
|
+
objects, clear seams, and UNIXy ergonomics.\n"
|
19
60
|
email:
|
20
61
|
- scott@sublayer.com
|
21
62
|
executables:
|
@@ -23,13 +64,26 @@ executables:
|
|
23
64
|
extensions: []
|
24
65
|
extra_rdoc_files: []
|
25
66
|
files:
|
67
|
+
- ".claude/settings.local.json"
|
26
68
|
- ".rspec"
|
69
|
+
- CLAUDE.md
|
27
70
|
- LICENSE.txt
|
28
71
|
- README.md
|
29
72
|
- Rakefile
|
30
73
|
- exe/airb
|
31
74
|
- lib/airb.rb
|
75
|
+
- lib/airb/organism.rb
|
76
|
+
- lib/airb/ports/chat_tty.rb
|
77
|
+
- lib/airb/systems/coordination.rb
|
78
|
+
- lib/airb/systems/governance.rb
|
79
|
+
- lib/airb/systems/identity.rb
|
80
|
+
- lib/airb/systems/intelligence.rb
|
81
|
+
- lib/airb/systems/monitoring.rb
|
82
|
+
- lib/airb/tools/fs/edit_file.rb
|
83
|
+
- lib/airb/tools/fs/list_files.rb
|
84
|
+
- lib/airb/tools/fs/read_file.rb
|
32
85
|
- lib/airb/version.rb
|
86
|
+
- llms.txt
|
33
87
|
- sig/airb.rbs
|
34
88
|
homepage: https://github.com/sublayerapp/airb
|
35
89
|
licenses:
|
@@ -38,7 +92,6 @@ metadata:
|
|
38
92
|
homepage_uri: https://github.com/sublayerapp/airb
|
39
93
|
source_code_uri: https://github.com/sublayerapp/airb
|
40
94
|
changelog_uri: https://github.com/sublayerapp/airb
|
41
|
-
rubygems_mfa_required: 'true'
|
42
95
|
rdoc_options: []
|
43
96
|
require_paths:
|
44
97
|
- lib
|
@@ -46,7 +99,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
99
|
requirements:
|
47
100
|
- - ">="
|
48
101
|
- !ruby/object:Gem::Version
|
49
|
-
version: 3.
|
102
|
+
version: '3.4'
|
50
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
104
|
requirements:
|
52
105
|
- - ">="
|
@@ -55,6 +108,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
108
|
requirements: []
|
56
109
|
rubygems_version: 3.6.9
|
57
110
|
specification_version: 4
|
58
|
-
summary:
|
59
|
-
by Stafford Beer’s Viable System Model.
|
111
|
+
summary: CLI-based programming agent for Ruby with VSM architecture
|
60
112
|
test_files: []
|