ollama_agent 0.1.0 → 0.3.0
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/.cursor/skills/ruby-code-review-levels/SKILL.md +115 -0
- data/.cursor/skills/self-improvement-sandbox-safety/SKILL.md +65 -0
- data/.env.example +25 -0
- data/CHANGELOG.md +40 -0
- data/README.md +135 -4
- data/docs/ARCHITECTURE.md +42 -0
- data/docs/PERFORMANCE.md +22 -0
- data/docs/SESSIONS.md +48 -0
- data/docs/TOOLS.md +53 -0
- data/docs/TOOL_RUNTIME.md +154 -0
- data/docs/superpowers/plans/2026-03-26-production-ready-ollama-agent.md +2454 -0
- data/docs/superpowers/specs/2026-03-26-production-ready-ollama-agent-design.md +400 -0
- data/lib/ollama_agent/agent/agent_config.rb +53 -0
- data/lib/ollama_agent/agent/client_wiring.rb +76 -0
- data/lib/ollama_agent/agent/prompt_wiring.rb +55 -0
- data/lib/ollama_agent/agent/session_wiring.rb +53 -0
- data/lib/ollama_agent/agent.rb +148 -73
- data/lib/ollama_agent/agent_prompt.rb +31 -1
- data/lib/ollama_agent/chat_stream_carry.rb +88 -0
- data/lib/ollama_agent/chat_stream_thinking_format.rb +29 -0
- data/lib/ollama_agent/cli.rb +394 -4
- data/lib/ollama_agent/console.rb +177 -5
- data/lib/ollama_agent/context/manager.rb +100 -0
- data/lib/ollama_agent/context/token_counter.rb +33 -0
- data/lib/ollama_agent/diff_path_validator.rb +32 -10
- data/lib/ollama_agent/env_config.rb +44 -0
- data/lib/ollama_agent/external_agents/TODO-plan.md +1948 -0
- data/lib/ollama_agent/external_agents/argv_interp.rb +21 -0
- data/lib/ollama_agent/external_agents/default_agents.yml +60 -0
- data/lib/ollama_agent/external_agents/delegate_logger.rb +31 -0
- data/lib/ollama_agent/external_agents/delegate_timeout_status.rb +12 -0
- data/lib/ollama_agent/external_agents/env_helpers.rb +38 -0
- data/lib/ollama_agent/external_agents/path_validator.rb +32 -0
- data/lib/ollama_agent/external_agents/probe.rb +122 -0
- data/lib/ollama_agent/external_agents/registry.rb +50 -0
- data/lib/ollama_agent/external_agents/runner.rb +118 -0
- data/lib/ollama_agent/external_agents.rb +9 -0
- data/lib/ollama_agent/global_dotenv.rb +39 -0
- data/lib/ollama_agent/model_env.rb +26 -0
- data/lib/ollama_agent/ollama_chat_thinking_stream.rb +41 -0
- data/lib/ollama_agent/ollama_connection.rb +6 -1
- data/lib/ollama_agent/patch_risk.rb +81 -0
- data/lib/ollama_agent/patch_support.rb +27 -1
- data/lib/ollama_agent/path_sandbox.rb +62 -0
- data/lib/ollama_agent/prompt_skills/clean_ruby.md +131 -0
- data/lib/ollama_agent/prompt_skills/code_review.md +112 -0
- data/lib/ollama_agent/prompt_skills/design_patterns.md +56 -0
- data/lib/ollama_agent/prompt_skills/manifest.yml +25 -0
- data/lib/ollama_agent/prompt_skills/ollama_agent_patterns.md +132 -0
- data/lib/ollama_agent/prompt_skills/rails_best_practices.md +41 -0
- data/lib/ollama_agent/prompt_skills/rails_style.md +138 -0
- data/lib/ollama_agent/prompt_skills/rspec.md +280 -0
- data/lib/ollama_agent/prompt_skills/rubocop.md +7 -0
- data/lib/ollama_agent/prompt_skills/ruby_style.md +121 -0
- data/lib/ollama_agent/prompt_skills/solid.md +270 -0
- data/lib/ollama_agent/prompt_skills/solid_ruby.md +223 -0
- data/lib/ollama_agent/prompt_skills.rb +169 -0
- data/lib/ollama_agent/repo_list.rb +4 -1
- data/lib/ollama_agent/resilience/audit_logger.rb +79 -0
- data/lib/ollama_agent/resilience/retry_middleware.rb +45 -0
- data/lib/ollama_agent/resilience/retry_policy.rb +51 -0
- data/lib/ollama_agent/ruby_index_tool_support.rb +17 -6
- data/lib/ollama_agent/runner.rb +123 -0
- data/lib/ollama_agent/sandboxed_tools/delegate_external.rb +62 -0
- data/lib/ollama_agent/sandboxed_tools/file_read_write.rb +100 -0
- data/lib/ollama_agent/sandboxed_tools/search_text.rb +60 -0
- data/lib/ollama_agent/sandboxed_tools.rb +55 -116
- data/lib/ollama_agent/search_backend.rb +93 -0
- data/lib/ollama_agent/self_improvement/analyzer.rb +34 -0
- data/lib/ollama_agent/self_improvement/improver.rb +340 -0
- data/lib/ollama_agent/self_improvement/modes.rb +25 -0
- data/lib/ollama_agent/self_improvement/ruby_mastery_context.rb +66 -0
- data/lib/ollama_agent/self_improvement.rb +5 -0
- data/lib/ollama_agent/session/session.rb +8 -0
- data/lib/ollama_agent/session/store.rb +68 -0
- data/lib/ollama_agent/streaming/console_streamer.rb +29 -0
- data/lib/ollama_agent/streaming/hooks.rb +39 -0
- data/lib/ollama_agent/tool_arguments.rb +13 -1
- data/lib/ollama_agent/tool_content_parser.rb +1 -1
- data/lib/ollama_agent/tool_runtime/executor.rb +34 -0
- data/lib/ollama_agent/tool_runtime/json_extractor.rb +62 -0
- data/lib/ollama_agent/tool_runtime/loop.rb +72 -0
- data/lib/ollama_agent/tool_runtime/memory.rb +32 -0
- data/lib/ollama_agent/tool_runtime/ollama_json_planner.rb +98 -0
- data/lib/ollama_agent/tool_runtime/plan_extractor.rb +12 -0
- data/lib/ollama_agent/tool_runtime/registry.rb +60 -0
- data/lib/ollama_agent/tool_runtime/tool.rb +24 -0
- data/lib/ollama_agent/tool_runtime.rb +24 -0
- data/lib/ollama_agent/tools/registry.rb +55 -0
- data/lib/ollama_agent/tools_schema.rb +74 -1
- data/lib/ollama_agent/user_prompt.rb +35 -0
- data/lib/ollama_agent/version.rb +1 -1
- data/lib/ollama_agent.rb +25 -0
- data/reproduce_429.rb +40 -0
- data/sig/ollama_agent.rbs +111 -1
- metadata +78 -2
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Production-Ready `ollama_agent` — Design Spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-26
|
|
4
|
+
**Status:** Approved
|
|
5
|
+
**Approach:** Additive Layers (B) — existing core untouched; six new layers stacked on top
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Goals
|
|
10
|
+
|
|
11
|
+
Transform `ollama_agent` v0.1.x from a solid single-developer CLI tool into a production-ready gem that serves three personas simultaneously:
|
|
12
|
+
|
|
13
|
+
- **Solo developer** — maximum autonomy, streaming, session resume, retries
|
|
14
|
+
- **Team/shared** — audit logs, safe defaults, structured observability
|
|
15
|
+
- **Library consumer** — stable `OllamaAgent::Runner` API, extensible tool registry, YARD docs
|
|
16
|
+
|
|
17
|
+
All new capabilities are **backward-compatible** and **opt-in** via new flags/env vars. No existing CLI flags, env vars, or `Agent.new` kwargs change.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Architecture Overview
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
CLI / Runner.run(query)
|
|
25
|
+
→ Session::Store.resume (if --resume) or Session::Store.new session header
|
|
26
|
+
→ Agent#run
|
|
27
|
+
→ Context::Manager.trim(messages)
|
|
28
|
+
→ OllamaConnection (with Resilience::RetryMiddleware)
|
|
29
|
+
→ Ollama::Client#chat (streaming: true when subscribed)
|
|
30
|
+
→ Streaming::Hooks.emit(:on_token, chunk)
|
|
31
|
+
→ Tools::Registry.execute(name, args)
|
|
32
|
+
→ Resilience::AuditLogger.log_tool_call(...)
|
|
33
|
+
→ repeat until no tool calls
|
|
34
|
+
→ Session::Store.save(messages)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### New directory structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
lib/ollama_agent/
|
|
41
|
+
├── agent.rb ← unchanged public API; gains hooks + context manager wiring
|
|
42
|
+
├── cli.rb ← gains --stream, --session, --resume flags
|
|
43
|
+
├── sandboxed_tools.rb ← gains write_file; case replaced by Registry dispatch
|
|
44
|
+
├── tools_schema.rb ← gains write_file schema entry
|
|
45
|
+
│
|
|
46
|
+
├── tools/
|
|
47
|
+
│ ├── registry.rb ← OllamaAgent::Tools.register / .execute / .schema_for
|
|
48
|
+
│ ├── built_in.rb ← re-registers existing 4 tools + write_file via registry
|
|
49
|
+
│ └── base.rb ← optional structured tool base class
|
|
50
|
+
│
|
|
51
|
+
├── context/
|
|
52
|
+
│ ├── manager.rb ← token budget, sliding window trim, summarize hook
|
|
53
|
+
│ └── token_counter.rb ← char-estimate default; tiktoken_ruby plug-in when present
|
|
54
|
+
│
|
|
55
|
+
├── session/
|
|
56
|
+
│ ├── store.rb ← save/load NDJSON under .ollama_agent/sessions/
|
|
57
|
+
│ └── session.rb ← session metadata + message envelope
|
|
58
|
+
│
|
|
59
|
+
├── streaming/
|
|
60
|
+
│ ├── hooks.rb ← on_token/on_chunk/on_tool_call/on_tool_result/on_complete/on_error
|
|
61
|
+
│ └── console_streamer.rb ← default CLI subscriber for live token output
|
|
62
|
+
│
|
|
63
|
+
├── resilience/
|
|
64
|
+
│ ├── retry_middleware.rb ← exponential backoff on TimeoutError / HTTP 5xx / 429
|
|
65
|
+
│ └── audit_logger.rb ← NDJSON to .ollama_agent/logs/ behind OLLAMA_AGENT_AUDIT=1
|
|
66
|
+
│
|
|
67
|
+
└── runner.rb ← OllamaAgent::Runner — stable library facade
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 3. Layer 1 — Tool Registry + `write_file`
|
|
73
|
+
|
|
74
|
+
### 3.1 `write_file` tool
|
|
75
|
+
|
|
76
|
+
- **Purpose:** create or overwrite a file under project root with full UTF-8 content
|
|
77
|
+
- **Schema:** `{ path: String (required), content: String (required) }`
|
|
78
|
+
- **Guards:** same `path_allowed?` sandbox as `edit_file`; blocked in read-only mode; confirmation flow when `confirm_patches: true` (prompt text: "Write file? (y/n)" — distinct from patch prompt)
|
|
79
|
+
- **Audit:** logged as `write_applied` event when audit enabled
|
|
80
|
+
- **Agent prompt addition:** "use `write_file` for new files or full rewrites; prefer `edit_file` for surgical changes"
|
|
81
|
+
|
|
82
|
+
### 3.2 `Tools::Registry`
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Public API
|
|
86
|
+
OllamaAgent::Tools.register(:name, schema: { ... }) { |args, context:| ... }
|
|
87
|
+
OllamaAgent::Tools.execute(name, args, context:)
|
|
88
|
+
OllamaAgent::Tools.all_schemas(read_only:, orchestrator:)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- Hash-based dispatch replaces `case` in `SandboxedTools#execute_tool`
|
|
92
|
+
- Built-in tools registered at require-time in `tools/built_in.rb`
|
|
93
|
+
- `context` hash passes `{ root:, read_only:, orchestrator: }` to every handler
|
|
94
|
+
- Custom tools registered before `Runner.build` are automatically injected into the model's tool list
|
|
95
|
+
- `TOOLS` / `READ_ONLY_TOOLS` constants kept as backward-compatible aliases
|
|
96
|
+
|
|
97
|
+
### 3.3 Constraints
|
|
98
|
+
|
|
99
|
+
- Custom tool handlers must respect `context[:read_only]`; registry enforces this for write-class built-ins
|
|
100
|
+
- No metaprogramming — plain `Hash` keyed by string name; explicit registration only
|
|
101
|
+
- Adding a tool does not require changes to `agent.rb`, `cli.rb`, or `tools_schema.rb`
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. Layer 2 — Context Manager
|
|
106
|
+
|
|
107
|
+
### 4.1 `Context::Manager`
|
|
108
|
+
|
|
109
|
+
Inserted into `Agent#execute_agent_turns` before every `chat` call. Never mutates messages in place — returns a trimmed copy.
|
|
110
|
+
|
|
111
|
+
**Configuration:**
|
|
112
|
+
|
|
113
|
+
| Env var | Keyword | Default |
|
|
114
|
+
|---------|---------|---------|
|
|
115
|
+
| `OLLAMA_AGENT_MAX_TOKENS` | `max_tokens:` | `8_192` |
|
|
116
|
+
| `OLLAMA_AGENT_CONTEXT_SUMMARIZE` | `context_summarize:` | `false` |
|
|
117
|
+
|
|
118
|
+
**Trim strategies:**
|
|
119
|
+
|
|
120
|
+
| Strategy | Behavior |
|
|
121
|
+
|----------|----------|
|
|
122
|
+
| Sliding window (default) | Drop oldest non-system message pairs until under `SUMMARY_THRESHOLD` (85%) of budget |
|
|
123
|
+
| Summarize (`context_summarize: true`) | Ask model to summarize dropped segment; inject as system message |
|
|
124
|
+
|
|
125
|
+
### 4.2 Invariants
|
|
126
|
+
|
|
127
|
+
- System prompt is **never** trimmed
|
|
128
|
+
- The most recent `user` message is **never** trimmed
|
|
129
|
+
- `tool` result messages are trimmed together with their preceding `assistant` message (never orphaned)
|
|
130
|
+
- When a single message exceeds budget: truncate with `[truncated by context manager]` suffix — same pattern as `Runner#truncate`
|
|
131
|
+
|
|
132
|
+
### 4.3 `Context::TokenCounter`
|
|
133
|
+
|
|
134
|
+
- Default: `chars / 4` (safe zero-dep estimate)
|
|
135
|
+
- Auto-upgrades to `tiktoken_ruby` when present in host bundle (`require` rescue'd)
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 5. Layer 3 — Session Persistence
|
|
140
|
+
|
|
141
|
+
### 5.1 Storage format
|
|
142
|
+
|
|
143
|
+
**Location:** `.ollama_agent/sessions/<YYYY-MM-DD>T<HH-MM-SS>_<id>.ndjson` under project root
|
|
144
|
+
|
|
145
|
+
**File format** (NDJSON — one JSON object per line):
|
|
146
|
+
```jsonc
|
|
147
|
+
// Line 1: session header
|
|
148
|
+
{"v":1,"id":"abc123","model":"gpt-oss:120b-cloud","root":"/proj","started_at":"2026-03-26T14:32:01Z"}
|
|
149
|
+
// Subsequent lines: message envelopes
|
|
150
|
+
{"role":"user","content":"Refactor the CLI","ts":"2026-03-26T14:32:02Z"}
|
|
151
|
+
{"role":"assistant","content":"I'll start by reading cli.rb...","tool_calls":[...],"ts":"..."}
|
|
152
|
+
{"role":"tool","name":"read_file","content":"...","ts":"..."}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 5.2 `Session::Store` API
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Session::Store.save(session_id:, root:, message:) # append one message (crash-safe)
|
|
159
|
+
Session::Store.load(session_id:, root:) # → Array<Hash>
|
|
160
|
+
Session::Store.list(root:) # → Array<SessionMeta>, newest first
|
|
161
|
+
Session::Store.resume(session_id:, root:) # → messages Array for Agent seeding
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5.3 CLI integration
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
ollama_agent ask --session my-refactor "Start refactoring the CLI"
|
|
168
|
+
ollama_agent ask --session my-refactor --resume
|
|
169
|
+
ollama_agent ask -i --session my-refactor --resume # REPL + resume
|
|
170
|
+
ollama_agent sessions # list sessions table
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`--resume` without `--session` resumes the most recent session for the current root.
|
|
174
|
+
|
|
175
|
+
### 5.4 Constraints
|
|
176
|
+
|
|
177
|
+
- Scoped to project root — not global
|
|
178
|
+
- Messages appended after each turn, not batch-written at end (crash-safe)
|
|
179
|
+
- `Context::Manager` trims loaded session messages before first chat call
|
|
180
|
+
- Files are plain text: human-readable, `grep`-able, `jq`-able
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 6. Layer 4 — Streaming
|
|
185
|
+
|
|
186
|
+
### 6.1 `Streaming::Hooks`
|
|
187
|
+
|
|
188
|
+
Lightweight event bus — plain Ruby, zero deps.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
hooks = OllamaAgent::Streaming::Hooks.new
|
|
192
|
+
hooks.on(:on_token) { |p| print p[:token] }
|
|
193
|
+
hooks.on(:on_tool_call) { |p| log(p[:name]) }
|
|
194
|
+
hooks.emit(:on_token, { token: "hello", turn: 1 })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Events:**
|
|
198
|
+
|
|
199
|
+
| Event | Payload keys | When fired |
|
|
200
|
+
|-------|-------------|------------|
|
|
201
|
+
| `on_token` | `token`, `turn` | Each streamed token |
|
|
202
|
+
| `on_chunk` | `delta`, `turn` | Each raw ollama-client chunk |
|
|
203
|
+
| `on_tool_call` | `name`, `args`, `turn` | Before tool executes |
|
|
204
|
+
| `on_tool_result` | `name`, `result`, `turn` | After tool returns |
|
|
205
|
+
| `on_complete` | `messages`, `turns` | Loop finished |
|
|
206
|
+
| `on_error` | `error`, `turn` | Unhandled error in loop |
|
|
207
|
+
| `on_retry` | `error`, `attempt`, `delay_ms` | RetryMiddleware fires |
|
|
208
|
+
|
|
209
|
+
Multiple subscribers per event are supported. All seven events are members of `Hooks::EVENTS`; `on_retry` is included even though it is fired by `RetryMiddleware` (not `Agent`) — the hooks bus is shared across layers.
|
|
210
|
+
|
|
211
|
+
### 6.2 `Streaming::ConsoleStreamer`
|
|
212
|
+
|
|
213
|
+
Default CLI subscriber. Auto-attached when stdout is a TTY and `OLLAMA_AGENT_STREAM=1` or `--stream` is passed.
|
|
214
|
+
|
|
215
|
+
### 6.3 `Agent` integration
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
def chat_assistant_message(messages)
|
|
219
|
+
if @hooks.subscribed?(:on_token)
|
|
220
|
+
stream_assistant_message(messages) # chunk-by-chunk path
|
|
221
|
+
else
|
|
222
|
+
block_assistant_message(messages) # existing behavior — 100% unchanged
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Non-streaming path is the default. Streaming is opt-in.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 7. Layer 5 — Resilience
|
|
232
|
+
|
|
233
|
+
### 7.1 `Resilience::RetryMiddleware`
|
|
234
|
+
|
|
235
|
+
Wraps `Ollama::Client#chat`. Applied in `OllamaConnection` / `Agent#build_default_client`.
|
|
236
|
+
|
|
237
|
+
**Retry policy:**
|
|
238
|
+
|
|
239
|
+
| Condition | Max attempts | Backoff |
|
|
240
|
+
|-----------|-------------|---------|
|
|
241
|
+
| `Ollama::TimeoutError` | 3 | Exponential: 2s, 4s, 8s + jitter |
|
|
242
|
+
| HTTP 503 / 429 | 3 | Same |
|
|
243
|
+
| `Errno::ECONNREFUSED` | 2 | 5s fixed |
|
|
244
|
+
| HTTP 4xx | 0 (fail immediately) | — |
|
|
245
|
+
| Tool errors | 0 (never retry) | — |
|
|
246
|
+
|
|
247
|
+
**Configuration:**
|
|
248
|
+
|
|
249
|
+
| Env var | Keyword | Default |
|
|
250
|
+
|---------|---------|---------|
|
|
251
|
+
| `OLLAMA_AGENT_MAX_RETRIES` | `max_retries:` | `3` |
|
|
252
|
+
| `OLLAMA_AGENT_RETRY_BASE_DELAY` | — | `2.0` seconds |
|
|
253
|
+
|
|
254
|
+
Set `OLLAMA_AGENT_MAX_RETRIES=0` to restore current no-retry behavior.
|
|
255
|
+
|
|
256
|
+
Fires `on_retry` hook on each attempt so AuditLogger and CLI can surface it.
|
|
257
|
+
|
|
258
|
+
### 7.2 `Resilience::AuditLogger`
|
|
259
|
+
|
|
260
|
+
**Location:** `.ollama_agent/logs/YYYY-MM-DD.ndjson` under project root
|
|
261
|
+
|
|
262
|
+
**Activation:** `OLLAMA_AGENT_AUDIT=1` or `audit: true` in `Runner.build`
|
|
263
|
+
|
|
264
|
+
**Logged events:**
|
|
265
|
+
|
|
266
|
+
```jsonc
|
|
267
|
+
{"ts":"...","event":"agent_start","model":"...","root":"...","session":"..."}
|
|
268
|
+
{"ts":"...","event":"tool_call","name":"read_file","args":{"path":"lib/cli.rb"},"turn":1}
|
|
269
|
+
{"ts":"...","event":"tool_result","name":"read_file","bytes":4210,"turn":1,"duration_ms":3}
|
|
270
|
+
{"ts":"...","event":"edit_applied","path":"lib/cli.rb","diff_lines":12,"turn":2}
|
|
271
|
+
{"ts":"...","event":"write_applied","path":"lib/new_file.rb","bytes":340,"turn":3}
|
|
272
|
+
{"ts":"...","event":"http_retry","attempt":2,"error":"TimeoutError","delay_ms":4213}
|
|
273
|
+
{"ts":"...","event":"agent_complete","turns":4,"duration_ms":38210}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Constraints:**
|
|
277
|
+
- Log writes are non-blocking best-effort (`rescue StandardError` around every write)
|
|
278
|
+
- Log dir created automatically on first write
|
|
279
|
+
- Daily rotation (one file per date)
|
|
280
|
+
- `OLLAMA_AGENT_AUDIT_MAX_RESULT_BYTES=4096` caps tool result bodies in log (default: 4096)
|
|
281
|
+
- Subscribes to `Streaming::Hooks` — no coupling to `Agent` internals
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 8. Layer 6 — Library API (`OllamaAgent::Runner`)
|
|
286
|
+
|
|
287
|
+
### 8.1 `Runner.build` factory
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
OllamaAgent::Runner.build(
|
|
291
|
+
root: Dir.pwd, # project root
|
|
292
|
+
model: nil, # OLLAMA_AGENT_MODEL or ollama-client default
|
|
293
|
+
stream: false, # enable streaming output
|
|
294
|
+
session_id: nil, # named session
|
|
295
|
+
resume: false, # load prior session messages
|
|
296
|
+
max_tokens: nil, # context budget (OLLAMA_AGENT_MAX_TOKENS)
|
|
297
|
+
context_summarize: false, # summarize vs sliding-window trim
|
|
298
|
+
max_retries: 3, # OLLAMA_AGENT_MAX_RETRIES
|
|
299
|
+
audit: false, # OLLAMA_AGENT_AUDIT
|
|
300
|
+
read_only: false, # disable edit_file + write_file
|
|
301
|
+
skills_enabled: true, # bundled prompt skills (matches Agent kwarg)
|
|
302
|
+
skill_paths: nil, # extra .md paths
|
|
303
|
+
confirm_patches: true, # prompt before applying patches
|
|
304
|
+
orchestrator: false, # enable external agent delegation
|
|
305
|
+
think: nil, # thinking mode
|
|
306
|
+
http_timeout: nil # OLLAMA_AGENT_TIMEOUT
|
|
307
|
+
) → OllamaAgent::Runner
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 8.2 Instance interface
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
runner.hooks # → Streaming::Hooks — attach subscribers before run
|
|
314
|
+
runner.session # → Session::Session or nil
|
|
315
|
+
runner.run(query) # → nil — execute one query
|
|
316
|
+
runner.start_repl # → nil — interactive REPL (blocks)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 8.3 SemVer contract (from 0.2.0)
|
|
320
|
+
|
|
321
|
+
| Surface | Stability |
|
|
322
|
+
|---------|-----------|
|
|
323
|
+
| `Runner.build` + `#run` + `#hooks` + `#session` | **Stable** |
|
|
324
|
+
| `OllamaAgent::Tools.register` | **Stable** |
|
|
325
|
+
| All `OLLAMA_AGENT_*` env vars | **Stable** |
|
|
326
|
+
| `OllamaAgent::CLI` flags | **Stable** |
|
|
327
|
+
| `OllamaAgent::Agent` kwargs | **Supported** (internal-friendly) |
|
|
328
|
+
| `Context/Session/Streaming/Resilience` internals | **Internal** |
|
|
329
|
+
|
|
330
|
+
### 8.4 Documentation
|
|
331
|
+
|
|
332
|
+
- `docs/ARCHITECTURE.md` — layer diagram + data flow
|
|
333
|
+
- `docs/TOOLS.md` — custom tool registration guide with examples
|
|
334
|
+
- `docs/SESSIONS.md` — session persistence usage and file format
|
|
335
|
+
- YARD `@param`/`@return` on all public `Runner`, `Tools.register`, and `Hooks#on` methods
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 9. New Environment Variables
|
|
340
|
+
|
|
341
|
+
| Variable | Purpose | Default |
|
|
342
|
+
|----------|---------|---------|
|
|
343
|
+
| `OLLAMA_AGENT_STREAM` | Enable streaming output | off |
|
|
344
|
+
| `OLLAMA_AGENT_MAX_TOKENS` | Context window budget | `8192` |
|
|
345
|
+
| `OLLAMA_AGENT_CONTEXT_SUMMARIZE` | Use summarize vs sliding-window trim | off |
|
|
346
|
+
| `OLLAMA_AGENT_MAX_RETRIES` | Max HTTP retry attempts (0 = disable) | `3` |
|
|
347
|
+
| `OLLAMA_AGENT_RETRY_BASE_DELAY` | Base backoff delay in seconds | `2.0` |
|
|
348
|
+
| `OLLAMA_AGENT_AUDIT` | Enable structured audit logging | off |
|
|
349
|
+
| `OLLAMA_AGENT_AUDIT_LOG_PATH` | Override audit log directory | `<root>/.ollama_agent/logs/` |
|
|
350
|
+
| `OLLAMA_AGENT_AUDIT_MAX_RESULT_BYTES` | Cap tool result bodies in audit log | `4096` |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 10. New CLI Flags
|
|
355
|
+
|
|
356
|
+
All new flags added to `ask`, `orchestrate`, `self_review`, `improve` where applicable:
|
|
357
|
+
|
|
358
|
+
| Flag | Purpose |
|
|
359
|
+
|------|---------|
|
|
360
|
+
| `--stream` | Enable streaming token output |
|
|
361
|
+
| `--session NAME` | Named session id |
|
|
362
|
+
| `--resume` | Load prior session messages before running |
|
|
363
|
+
| `--max-tokens N` | Context window budget |
|
|
364
|
+
| `--max-retries N` | HTTP retry limit |
|
|
365
|
+
| `--audit` | Enable audit logging for this run |
|
|
366
|
+
|
|
367
|
+
New top-level command: `ollama_agent sessions` — list sessions for current project root.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 11. Test Coverage Requirements
|
|
372
|
+
|
|
373
|
+
Each new layer ships with specs:
|
|
374
|
+
|
|
375
|
+
| Layer | Required specs |
|
|
376
|
+
|-------|---------------|
|
|
377
|
+
| Tool Registry | register, execute, all_schemas; custom tool injection; read-only guard |
|
|
378
|
+
| `write_file` | create, overwrite, path sandbox, read-only block, confirmation |
|
|
379
|
+
| Context::Manager | trim sliding window, trim summarize, invariants (system/last-user never trimmed) |
|
|
380
|
+
| Session::Store | save, load, list, resume, crash-safe append |
|
|
381
|
+
| Streaming::Hooks | on/emit, multiple subscribers, unknown event no-op |
|
|
382
|
+
| ConsoleStreamer | attaches correct handlers |
|
|
383
|
+
| RetryMiddleware | retries on timeout/503/429, no retry on 4xx, max attempts, backoff |
|
|
384
|
+
| AuditLogger | writes NDJSON, best-effort (write failure doesn't raise), daily rotation |
|
|
385
|
+
| Runner | build factory, run, hooks, session wiring; integration smoke test |
|
|
386
|
+
|
|
387
|
+
Existing specs must remain green with zero changes.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 12. Implementation Order
|
|
392
|
+
|
|
393
|
+
Each layer is an independently shippable PR:
|
|
394
|
+
|
|
395
|
+
1. **Tool Registry + `write_file`** — foundational; unblocks custom tools
|
|
396
|
+
2. **Streaming + Hooks** — unblocks AuditLogger (shares hook bus)
|
|
397
|
+
3. **Resilience (Retry + AuditLogger)** — depends on Hooks
|
|
398
|
+
4. **Context::Manager** — independent
|
|
399
|
+
5. **Session::Store** — independent; integrates with Context::Manager
|
|
400
|
+
6. **Runner + Library API + YARD docs** — integrates all layers; final PR
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
class Agent
|
|
5
|
+
# Value object grouping Agent construction options (Runner and tests build this explicitly).
|
|
6
|
+
class AgentConfig
|
|
7
|
+
attr_reader :model, :root, :confirm_patches, :http_timeout, :think, :read_only, :patch_policy,
|
|
8
|
+
:skill_paths, :skills_enabled, :skills_include, :skills_exclude, :external_skills_enabled,
|
|
9
|
+
:orchestrator, :confirm_delegation, :max_retries, :audit, :session_id, :resume,
|
|
10
|
+
:max_tokens, :context_summarize, :stdin, :stdout
|
|
11
|
+
|
|
12
|
+
# @param confirm_delegation [Boolean, nil] nil means default true
|
|
13
|
+
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/AbcSize -- value object mirrors Agent keywords
|
|
14
|
+
def initialize(model: nil, root: nil, confirm_patches: true, http_timeout: nil, think: nil,
|
|
15
|
+
read_only: false, patch_policy: nil,
|
|
16
|
+
skill_paths: nil, skills_enabled: nil, skills_include: nil, skills_exclude: nil,
|
|
17
|
+
external_skills_enabled: nil,
|
|
18
|
+
orchestrator: false, confirm_delegation: nil,
|
|
19
|
+
max_retries: nil, audit: nil,
|
|
20
|
+
session_id: nil, resume: false,
|
|
21
|
+
max_tokens: nil, context_summarize: nil,
|
|
22
|
+
stdin: $stdin, stdout: $stdout)
|
|
23
|
+
@model = model
|
|
24
|
+
@root = root
|
|
25
|
+
@confirm_patches = confirm_patches
|
|
26
|
+
@http_timeout = http_timeout
|
|
27
|
+
@think = think
|
|
28
|
+
@read_only = read_only
|
|
29
|
+
@patch_policy = patch_policy
|
|
30
|
+
@skill_paths = skill_paths
|
|
31
|
+
@skills_enabled = skills_enabled
|
|
32
|
+
@skills_include = skills_include
|
|
33
|
+
@skills_exclude = skills_exclude
|
|
34
|
+
@external_skills_enabled = external_skills_enabled
|
|
35
|
+
@orchestrator = orchestrator
|
|
36
|
+
@confirm_delegation = confirm_delegation
|
|
37
|
+
@max_retries = max_retries
|
|
38
|
+
@audit = audit
|
|
39
|
+
@session_id = session_id
|
|
40
|
+
@resume = resume
|
|
41
|
+
@max_tokens = max_tokens
|
|
42
|
+
@context_summarize = context_summarize
|
|
43
|
+
@stdin = stdin
|
|
44
|
+
@stdout = stdout
|
|
45
|
+
end
|
|
46
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists, Metrics/AbcSize
|
|
47
|
+
|
|
48
|
+
def resolved_confirm_delegation
|
|
49
|
+
@confirm_delegation.nil? || @confirm_delegation
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
class Agent
|
|
5
|
+
# HTTP client construction, timeouts, retries, and audit logger attachment.
|
|
6
|
+
module ClientWiring
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/MethodLength
|
|
10
|
+
def build_default_client
|
|
11
|
+
config = Ollama::Config.new
|
|
12
|
+
@http_timeout_seconds = resolved_http_timeout_seconds
|
|
13
|
+
config.timeout = @http_timeout_seconds
|
|
14
|
+
OllamaConnection.apply_env_to_config(config)
|
|
15
|
+
ollama_client = Ollama::Client.new(config: config)
|
|
16
|
+
Resilience::RetryMiddleware.new(
|
|
17
|
+
client: ollama_client,
|
|
18
|
+
max_attempts: resolved_max_retries,
|
|
19
|
+
hooks: @hooks,
|
|
20
|
+
base_delay: resolved_retry_base_delay
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable Metrics/MethodLength
|
|
24
|
+
|
|
25
|
+
def resolved_max_retries
|
|
26
|
+
return @max_retries unless @max_retries.nil?
|
|
27
|
+
|
|
28
|
+
EnvConfig.fetch_int(
|
|
29
|
+
"OLLAMA_AGENT_MAX_RETRIES",
|
|
30
|
+
Resilience::RetryMiddleware::DEFAULT_MAX_ATTEMPTS,
|
|
31
|
+
strict: EnvConfig.strict_env?
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolved_retry_base_delay
|
|
36
|
+
EnvConfig.fetch_float(
|
|
37
|
+
"OLLAMA_AGENT_RETRY_BASE_DELAY",
|
|
38
|
+
Resilience::RetryMiddleware::DEFAULT_BASE_DELAY,
|
|
39
|
+
strict: EnvConfig.strict_env?
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolved_http_timeout_seconds
|
|
44
|
+
parsed = TimeoutParam.parse_positive(@http_timeout_override)
|
|
45
|
+
return parsed if parsed
|
|
46
|
+
|
|
47
|
+
raw = ENV.fetch("OLLAMA_AGENT_TIMEOUT", nil)
|
|
48
|
+
parsed = TimeoutParam.parse_positive(raw)
|
|
49
|
+
EnvConfig.warn_invalid("OLLAMA_AGENT_TIMEOUT", raw, DEFAULT_HTTP_TIMEOUT) if malformed_timeout_env?(raw, parsed)
|
|
50
|
+
|
|
51
|
+
parsed || DEFAULT_HTTP_TIMEOUT
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def malformed_timeout_env?(raw, parsed)
|
|
55
|
+
raw && !raw.to_s.strip.empty? && parsed.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolved_audit_enabled
|
|
59
|
+
return @audit unless @audit.nil?
|
|
60
|
+
|
|
61
|
+
ENV.fetch("OLLAMA_AGENT_AUDIT", "0") == "1"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def audit_log_dir
|
|
65
|
+
custom = ENV.fetch("OLLAMA_AGENT_AUDIT_LOG_PATH", nil)
|
|
66
|
+
return custom if custom && !custom.to_s.strip.empty?
|
|
67
|
+
|
|
68
|
+
File.join(@root, ".ollama_agent", "logs")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def attach_audit_logger
|
|
72
|
+
Resilience::AuditLogger.new(log_dir: audit_log_dir, hooks: @hooks).attach
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
class Agent
|
|
5
|
+
# System prompt and bundled skill resolution.
|
|
6
|
+
module PromptWiring
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/MethodLength
|
|
10
|
+
def system_prompt
|
|
11
|
+
base = @read_only ? AgentPrompt.self_review_text : AgentPrompt.text
|
|
12
|
+
composed = PromptSkills.compose(
|
|
13
|
+
base: base,
|
|
14
|
+
skills_enabled: resolved_skills_enabled,
|
|
15
|
+
skills_include: resolved_skills_include,
|
|
16
|
+
skills_exclude: resolved_skills_exclude,
|
|
17
|
+
skill_paths: resolved_skill_paths,
|
|
18
|
+
external_skills_enabled: resolved_external_skills_enabled
|
|
19
|
+
)
|
|
20
|
+
return composed unless @orchestrator
|
|
21
|
+
|
|
22
|
+
[composed, AgentPrompt.orchestrator_addon].join("\n\n---\n\n")
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Metrics/MethodLength
|
|
25
|
+
|
|
26
|
+
def resolved_skills_enabled
|
|
27
|
+
return @skills_enabled unless @skills_enabled.nil?
|
|
28
|
+
|
|
29
|
+
PromptSkills.env_truthy("OLLAMA_AGENT_SKILLS", default: true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolved_skills_include
|
|
33
|
+
return @skills_include unless @skills_include.nil?
|
|
34
|
+
|
|
35
|
+
PromptSkills.parse_id_list(ENV.fetch("OLLAMA_AGENT_SKILLS_INCLUDE", nil))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolved_skills_exclude
|
|
39
|
+
return @skills_exclude unless @skills_exclude.nil?
|
|
40
|
+
|
|
41
|
+
PromptSkills.parse_id_list(ENV.fetch("OLLAMA_AGENT_SKILLS_EXCLUDE", nil))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolved_skill_paths
|
|
45
|
+
@skill_paths
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolved_external_skills_enabled
|
|
49
|
+
return @external_skills_enabled unless @external_skills_enabled.nil?
|
|
50
|
+
|
|
51
|
+
PromptSkills.env_truthy("OLLAMA_AGENT_EXTERNAL_SKILLS", default: true)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
class Agent
|
|
5
|
+
# Session resume/save and tool message formatting for the chat transcript.
|
|
6
|
+
module SessionWiring
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
10
|
+
def build_messages_for_run(query)
|
|
11
|
+
prior = @session_id && @resume ? Session::Store.resume(session_id: @session_id, root: @root) : []
|
|
12
|
+
messages = prior.empty? ? [{ role: "system", content: system_prompt }] : prior
|
|
13
|
+
first = messages.first
|
|
14
|
+
unless first && (first[:role] == "system" || first["role"] == "system")
|
|
15
|
+
messages.unshift({ role: "system", content: system_prompt })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
messages << { role: "user", content: query }
|
|
19
|
+
Session::Store.save(session_id: @session_id, root: @root, message: messages.last) if @session_id
|
|
20
|
+
|
|
21
|
+
messages
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
24
|
+
|
|
25
|
+
def append_tool_results(messages, tool_calls)
|
|
26
|
+
tool_calls.each do |tool_call|
|
|
27
|
+
@hooks.emit(:on_tool_call, { name: tool_call.name, args: tool_call.arguments || {}, turn: current_turn })
|
|
28
|
+
result = execute_tool(tool_call.name, tool_call.arguments || {})
|
|
29
|
+
@hooks.emit(:on_tool_result, { name: tool_call.name, result: result.to_s, turn: current_turn })
|
|
30
|
+
messages << tool_message(tool_call, result)
|
|
31
|
+
save_message_to_session(messages.last)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save_message_to_session(msg)
|
|
36
|
+
return unless @session_id
|
|
37
|
+
|
|
38
|
+
Session::Store.save(session_id: @session_id, root: @root, message: msg)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tool_message(tool_call, result)
|
|
42
|
+
msg = {
|
|
43
|
+
role: "tool",
|
|
44
|
+
name: tool_call.name,
|
|
45
|
+
content: result.to_s
|
|
46
|
+
}
|
|
47
|
+
id = tool_call.id
|
|
48
|
+
msg[:tool_call_id] = id if id && !id.to_s.empty?
|
|
49
|
+
msg
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|