ollama_agent 0.3.0 → 1.0.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/CHANGELOG.md +23 -0
- data/README.md +14 -3
- data/lib/ollama_agent/agent/agent_config.rb +19 -2
- data/lib/ollama_agent/agent/client_wiring.rb +3 -8
- data/lib/ollama_agent/agent/session_wiring.rb +37 -3
- data/lib/ollama_agent/agent.rb +82 -6
- data/lib/ollama_agent/cli/repl.rb +159 -0
- data/lib/ollama_agent/cli/repl_shared.rb +229 -0
- data/lib/ollama_agent/cli/tui_repl.rb +149 -0
- data/lib/ollama_agent/cli.rb +129 -49
- data/lib/ollama_agent/core/action_envelope.rb +82 -0
- data/lib/ollama_agent/core/budget.rb +90 -0
- data/lib/ollama_agent/core/loop_detector.rb +67 -0
- data/lib/ollama_agent/core/schema_validator.rb +136 -0
- data/lib/ollama_agent/core/trace_logger.rb +138 -0
- data/lib/ollama_agent/external_agents/probe.rb +23 -3
- data/lib/ollama_agent/indexing/context_packer.rb +140 -0
- data/lib/ollama_agent/indexing/diff_summarizer.rb +125 -0
- data/lib/ollama_agent/indexing/file_indexer.rb +129 -0
- data/lib/ollama_agent/indexing/repo_scanner.rb +158 -0
- data/lib/ollama_agent/memory/long_term.rb +109 -0
- data/lib/ollama_agent/memory/manager.rb +121 -0
- data/lib/ollama_agent/memory/session_memory.rb +93 -0
- data/lib/ollama_agent/memory/short_term.rb +66 -0
- data/lib/ollama_agent/ollama_cloud_catalog.rb +66 -0
- data/lib/ollama_agent/ollama_connection.rb +30 -0
- data/lib/ollama_agent/plugins/loader.rb +95 -0
- data/lib/ollama_agent/plugins/registry.rb +103 -0
- data/lib/ollama_agent/providers/anthropic.rb +245 -0
- data/lib/ollama_agent/providers/base.rb +79 -0
- data/lib/ollama_agent/providers/ollama.rb +118 -0
- data/lib/ollama_agent/providers/openai.rb +215 -0
- data/lib/ollama_agent/providers/registry.rb +76 -0
- data/lib/ollama_agent/providers/router.rb +93 -0
- data/lib/ollama_agent/resilience/retry_middleware.rb +5 -0
- data/lib/ollama_agent/runner.rb +25 -4
- data/lib/ollama_agent/runtime/approval_gate.rb +74 -0
- data/lib/ollama_agent/runtime/permissions.rb +103 -0
- data/lib/ollama_agent/runtime/policies.rb +100 -0
- data/lib/ollama_agent/runtime/sandbox.rb +130 -0
- data/lib/ollama_agent/streaming/hooks.rb +3 -1
- data/lib/ollama_agent/tools/base.rb +108 -0
- data/lib/ollama_agent/tools/git_tools.rb +176 -0
- data/lib/ollama_agent/tools/http_tools.rb +202 -0
- data/lib/ollama_agent/tools/memory_tools.rb +116 -0
- data/lib/ollama_agent/tools/shell_tools.rb +208 -0
- data/lib/ollama_agent/tui.rb +183 -0
- data/lib/ollama_agent/tui_slash_reader.rb +147 -0
- data/lib/ollama_agent/tui_user_prompt.rb +45 -0
- data/lib/ollama_agent/version.rb +1 -1
- data/lib/ollama_agent.rb +46 -1
- metadata +142 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69d5c209f38109fb75cd33524df7cf31af937dbaa5b7b0c493973c6e7cc16c41
|
|
4
|
+
data.tar.gz: ef92101a3923debec6b193b1c9e56d1817e6154b44874daa0364a564d2336724
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f71a58b11c1a16eb5310313c8e3036323684861bd594c4c1dfc114504ca9f4a4b6cec3cef0309476d9be607aafd09fbaf01d4c5f8cea77b789708b7f8874ffe
|
|
7
|
+
data.tar.gz: e3f9747d242a68858beca29fc643711057f04d36f0bbd57929001b4feae56d8b6acd45969abf1adc64b57522f571e1f3e254142896c82b398cc21f0f9bced0e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-04-11
|
|
4
|
+
|
|
5
|
+
First **stable** release under [Semantic Versioning](https://semver.org/): within the **`1.x`** series, the documented public Ruby API and CLI behavior are intended to remain **backwards compatible** except where called out in the changelog.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **CLI default:** `ollama_agent` with no subcommand runs **`ask`** with the **interactive TUI** when no query is given (same as `ollama_agent ask` with no query). Use **`ask -i` without `--tui`** for the plain line REPL, **`ask "…"`** for a one-shot task, or another subcommand (`self_review`, `sessions`, …) for other workflows.
|
|
10
|
+
|
|
11
|
+
### Breaking changes (from 0.3.x)
|
|
12
|
+
|
|
13
|
+
- **`tty-spinner` removed** from the gem; **`OllamaAgent::TUI`** no longer exposes `with_spinner`, `dismiss_progress_spinner`, `pause_progress_for_user_prompt`, or `resume_progress_after_user_prompt`. Code that called those methods or relied on the transitive `tty-spinner` dependency must be updated.
|
|
14
|
+
- **`Agent#chat_assistant_message`** uses the streaming HTTP path **only** when something subscribes to **`on_token`** (not merely **`on_thinking`**). Integrations that registered only `on_thinking` expecting streamed chat must also subscribe to `on_token` or use another supported hook.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- TUI slash-command tab completion no longer raises `FrozenError` when editing a completed frozen candidate (e.g. `/help`).
|
|
19
|
+
|
|
3
20
|
## [0.3.0] - 2026-04-06
|
|
4
21
|
|
|
5
22
|
### Added
|
|
23
|
+
|
|
6
24
|
- `ToolRuntime` — JSON plan loop for custom tools (`OllamaJsonPlanner`, registry, executor); see `docs/TOOL_RUNTIME.md`
|
|
7
25
|
- Optional **ruby_mastery** context for `self_review` / `improve` (`OLLAMA_AGENT_RUBY_MASTERY`, `--no-ruby-mastery`)
|
|
8
26
|
- `OllamaAgent::ModelEnv` — shared model name resolution from environment
|
|
@@ -11,14 +29,17 @@
|
|
|
11
29
|
- External agents / argv expansion and related orchestration refinements
|
|
12
30
|
|
|
13
31
|
### Changed
|
|
32
|
+
|
|
14
33
|
- `SearchBackend` finds `rg` / `grep` by scanning `PATH` (avoids relying on a `command` executable on trimmed `PATH`)
|
|
15
34
|
|
|
16
35
|
### Fixed
|
|
36
|
+
|
|
17
37
|
- `SelfImprovement::Improver#run` accepts `max_tokens` and `context_summarize` from the CLI (Ruby 3 keyword compatibility)
|
|
18
38
|
|
|
19
39
|
## [0.2.0] - 2026-03-26
|
|
20
40
|
|
|
21
41
|
### Added
|
|
42
|
+
|
|
22
43
|
- `write_file` tool — create or overwrite files (complements `edit_file` for surgical diffs)
|
|
23
44
|
- `OllamaAgent::Tools.register` — extensible tool registry for library consumers
|
|
24
45
|
- `Streaming::Hooks` — event bus (`on_token`, `on_tool_call`, `on_tool_result`, `on_complete`, `on_error`, `on_retry`)
|
|
@@ -32,10 +53,12 @@
|
|
|
32
53
|
- `docs/ARCHITECTURE.md`, `docs/TOOLS.md`, `docs/SESSIONS.md`
|
|
33
54
|
|
|
34
55
|
### Changed
|
|
56
|
+
|
|
35
57
|
- `READ_ONLY_TOOLS` now excludes both `edit_file` and `write_file`
|
|
36
58
|
- `Agent` now exposes `#hooks` (`Streaming::Hooks`) and `#session_id`
|
|
37
59
|
|
|
38
60
|
### New environment variables
|
|
61
|
+
|
|
39
62
|
- `OLLAMA_AGENT_STREAM`, `OLLAMA_AGENT_MAX_TOKENS`
|
|
40
63
|
- `OLLAMA_AGENT_MAX_RETRIES`, `OLLAMA_AGENT_RETRY_BASE_DELAY`
|
|
41
64
|
- `OLLAMA_AGENT_AUDIT`, `OLLAMA_AGENT_AUDIT_LOG_PATH`
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ollama_agent
|
|
2
2
|
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
|
|
5
5
|
Ruby gem that runs a **CLI coding agent** against a local [Ollama](https://ollama.com) model. It exposes tools to **list files**, **read files**, **search the tree** (ripgrep or grep), and **apply unified diffs** so the model can make small, reviewable edits.
|
|
6
6
|
|
|
@@ -40,6 +40,16 @@ bundle install
|
|
|
40
40
|
|
|
41
41
|
## Usage
|
|
42
42
|
|
|
43
|
+
**Default:** run the gem with **no subcommand** to open the **interactive TUI** (same as `ask` with no query):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ollama_agent
|
|
47
|
+
# or from this repo:
|
|
48
|
+
bundle exec ruby exe/ollama_agent
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Other entry points are **opt-in**: pass a **subcommand** (`self_review`, `sessions`, …) or **`ask` / `orchestrate`** with a **query** for a one-shot task, or flags for a plain line REPL (see below).
|
|
52
|
+
|
|
43
53
|
From the project you want the agent to modify (set the working directory accordingly):
|
|
44
54
|
|
|
45
55
|
```bash
|
|
@@ -63,10 +73,11 @@ Long-running models (slow local inference):
|
|
|
63
73
|
bundle exec ruby exe/ollama_agent ask --timeout 300 "Your task"
|
|
64
74
|
```
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
**Plain line REPL** (no TUI boxes / markdown shell): use **`ask` (or `orchestrate`) with `-i` and without `--tui`**—for example when you omit the query you must opt out of the default TUI this way:
|
|
67
77
|
|
|
68
78
|
```bash
|
|
69
79
|
bundle exec ruby exe/ollama_agent ask --interactive
|
|
80
|
+
# same idea: explicit -i, no --tui
|
|
70
81
|
```
|
|
71
82
|
|
|
72
83
|
Self-review modes (default project root is the **current working directory** unless you set `--root` or `OLLAMA_AGENT_ROOT`):
|
|
@@ -271,7 +282,7 @@ Repository **secrets** (Settings → Secrets and variables → Actions):
|
|
|
271
282
|
Release steps:
|
|
272
283
|
|
|
273
284
|
1. Bump `OllamaAgent::VERSION` in `lib/ollama_agent/version.rb` and commit to `main`.
|
|
274
|
-
2. Tag: `git tag
|
|
285
|
+
2. Tag: `git tag v1.0.0` (must match the version string) and `git push origin v1.0.0`.
|
|
275
286
|
|
|
276
287
|
## License
|
|
277
288
|
|
|
@@ -7,7 +7,10 @@ module OllamaAgent
|
|
|
7
7
|
attr_reader :model, :root, :confirm_patches, :http_timeout, :think, :read_only, :patch_policy,
|
|
8
8
|
:skill_paths, :skills_enabled, :skills_include, :skills_exclude, :external_skills_enabled,
|
|
9
9
|
:orchestrator, :confirm_delegation, :max_retries, :audit, :session_id, :resume,
|
|
10
|
-
:max_tokens, :context_summarize, :stdin, :stdout
|
|
10
|
+
:max_tokens, :context_summarize, :stdin, :stdout, :user_prompt,
|
|
11
|
+
# v2 platform options
|
|
12
|
+
:provider, :provider_name, :budget, :permissions, :policies,
|
|
13
|
+
:memory_manager, :trace_logger, :approval_gate
|
|
11
14
|
|
|
12
15
|
# @param confirm_delegation [Boolean, nil] nil means default true
|
|
13
16
|
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/AbcSize -- value object mirrors Agent keywords
|
|
@@ -19,7 +22,11 @@ module OllamaAgent
|
|
|
19
22
|
max_retries: nil, audit: nil,
|
|
20
23
|
session_id: nil, resume: false,
|
|
21
24
|
max_tokens: nil, context_summarize: nil,
|
|
22
|
-
stdin: $stdin, stdout: $stdout
|
|
25
|
+
stdin: $stdin, stdout: $stdout, user_prompt: nil,
|
|
26
|
+
# v2 platform options (all optional — nil keeps existing behaviour)
|
|
27
|
+
provider: nil, provider_name: nil, budget: nil,
|
|
28
|
+
permissions: nil, policies: nil,
|
|
29
|
+
memory_manager: nil, trace_logger: nil, approval_gate: nil)
|
|
23
30
|
@model = model
|
|
24
31
|
@root = root
|
|
25
32
|
@confirm_patches = confirm_patches
|
|
@@ -42,6 +49,16 @@ module OllamaAgent
|
|
|
42
49
|
@context_summarize = context_summarize
|
|
43
50
|
@stdin = stdin
|
|
44
51
|
@stdout = stdout
|
|
52
|
+
@user_prompt = user_prompt
|
|
53
|
+
# v2 platform options
|
|
54
|
+
@provider = provider
|
|
55
|
+
@provider_name = provider_name
|
|
56
|
+
@budget = budget
|
|
57
|
+
@permissions = permissions
|
|
58
|
+
@policies = policies
|
|
59
|
+
@memory_manager = memory_manager
|
|
60
|
+
@trace_logger = trace_logger
|
|
61
|
+
@approval_gate = approval_gate
|
|
45
62
|
end
|
|
46
63
|
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists, Metrics/AbcSize
|
|
47
64
|
|
|
@@ -6,21 +6,16 @@ module OllamaAgent
|
|
|
6
6
|
module ClientWiring
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
# rubocop:disable Metrics/MethodLength
|
|
10
9
|
def build_default_client
|
|
11
|
-
config = Ollama::Config.new
|
|
12
10
|
@http_timeout_seconds = resolved_http_timeout_seconds
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ollama_client = Ollama::Client.new(config: config)
|
|
16
|
-
Resilience::RetryMiddleware.new(
|
|
17
|
-
client: ollama_client,
|
|
11
|
+
OllamaConnection.retry_wrapped_client(
|
|
12
|
+
timeout: @http_timeout_seconds,
|
|
18
13
|
max_attempts: resolved_max_retries,
|
|
14
|
+
base_url: nil,
|
|
19
15
|
hooks: @hooks,
|
|
20
16
|
base_delay: resolved_retry_base_delay
|
|
21
17
|
)
|
|
22
18
|
end
|
|
23
|
-
# rubocop:enable Metrics/MethodLength
|
|
24
19
|
|
|
25
20
|
def resolved_max_retries
|
|
26
21
|
return @max_retries unless @max_retries.nil?
|
|
@@ -24,14 +24,48 @@ module OllamaAgent
|
|
|
24
24
|
|
|
25
25
|
def append_tool_results(messages, tool_calls)
|
|
26
26
|
tool_calls.each do |tool_call|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
name = tool_call.name
|
|
28
|
+
args = tool_call.arguments || {}
|
|
29
|
+
|
|
30
|
+
@hooks.emit(:on_tool_call, { name: name, args: args, turn: current_turn })
|
|
31
|
+
@loop_detector&.record!(name, args)
|
|
32
|
+
|
|
33
|
+
result = platform_guarded_tool_call(name, args)
|
|
34
|
+
|
|
35
|
+
@hooks.emit(:on_tool_result, { name: name, result: result.to_s, turn: current_turn })
|
|
36
|
+
@memory_manager&.record_tool_call(name, args, result)
|
|
30
37
|
messages << tool_message(tool_call, result)
|
|
31
38
|
save_message_to_session(messages.last)
|
|
32
39
|
end
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# Run permission / policy guards before delegating to execute_tool.
|
|
43
|
+
def platform_guarded_tool_call(name, args)
|
|
44
|
+
ctx = build_tool_context
|
|
45
|
+
|
|
46
|
+
# Permission check
|
|
47
|
+
if @permissions && !@permissions.allowed?(name)
|
|
48
|
+
return "Permission denied: tool '#{name}' is not allowed under the current permission profile (#{@permissions.profile})."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Policy check
|
|
52
|
+
if @policies
|
|
53
|
+
rejection = @policies.evaluate(name, args, ctx)
|
|
54
|
+
return rejection if rejection
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
execute_tool(name, args)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_tool_context
|
|
61
|
+
{
|
|
62
|
+
root: @root,
|
|
63
|
+
read_only: @read_only,
|
|
64
|
+
memory_manager: @memory_manager,
|
|
65
|
+
shell_call_count: @shell_call_count || 0
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
35
69
|
def save_message_to_session(msg)
|
|
36
70
|
return unless @session_id
|
|
37
71
|
|
data/lib/ollama_agent/agent.rb
CHANGED
|
@@ -16,10 +16,14 @@ require_relative "context/manager"
|
|
|
16
16
|
require_relative "session/store"
|
|
17
17
|
require_relative "env_config"
|
|
18
18
|
require_relative "model_env"
|
|
19
|
+
require_relative "ollama_cloud_catalog"
|
|
19
20
|
require_relative "agent/agent_config"
|
|
20
21
|
require_relative "agent/client_wiring"
|
|
21
22
|
require_relative "agent/prompt_wiring"
|
|
22
23
|
require_relative "agent/session_wiring"
|
|
24
|
+
require_relative "core/budget"
|
|
25
|
+
require_relative "core/loop_detector"
|
|
26
|
+
require_relative "core/trace_logger"
|
|
23
27
|
|
|
24
28
|
module OllamaAgent
|
|
25
29
|
# Runs a tool-calling loop against Ollama: read files, search, apply unified diffs.
|
|
@@ -34,7 +38,7 @@ module OllamaAgent
|
|
|
34
38
|
MAX_TURNS = 64
|
|
35
39
|
DEFAULT_HTTP_TIMEOUT = 120
|
|
36
40
|
|
|
37
|
-
attr_reader :client, :root, :hooks
|
|
41
|
+
attr_reader :client, :root, :hooks, :model
|
|
38
42
|
|
|
39
43
|
# @param config [AgentConfig, nil] when set, keyword options are ignored (use {Runner} or build {AgentConfig}).
|
|
40
44
|
# rubocop:disable Metrics/ParameterLists
|
|
@@ -48,7 +52,10 @@ module OllamaAgent
|
|
|
48
52
|
max_retries: nil, audit: nil,
|
|
49
53
|
session_id: nil, resume: false,
|
|
50
54
|
max_tokens: nil, context_summarize: nil,
|
|
51
|
-
stdin: $stdin, stdout: $stdout
|
|
55
|
+
stdin: $stdin, stdout: $stdout,
|
|
56
|
+
provider: nil, provider_name: nil, budget: nil,
|
|
57
|
+
permissions: nil, policies: nil,
|
|
58
|
+
memory_manager: nil, trace_logger: nil, approval_gate: nil, user_prompt: nil)
|
|
52
59
|
cfg = config || AgentConfig.new(
|
|
53
60
|
model: model, root: root, confirm_patches: confirm_patches, http_timeout: http_timeout, think: think,
|
|
54
61
|
read_only: read_only, patch_policy: patch_policy,
|
|
@@ -56,10 +63,14 @@ module OllamaAgent
|
|
|
56
63
|
skills_exclude: skills_exclude, external_skills_enabled: external_skills_enabled,
|
|
57
64
|
orchestrator: orchestrator, confirm_delegation: confirm_delegation,
|
|
58
65
|
max_retries: max_retries, audit: audit, session_id: session_id, resume: resume,
|
|
59
|
-
max_tokens: max_tokens, context_summarize: context_summarize, stdin: stdin, stdout: stdout
|
|
66
|
+
max_tokens: max_tokens, context_summarize: context_summarize, stdin: stdin, stdout: stdout,
|
|
67
|
+
provider: provider, provider_name: provider_name, budget: budget,
|
|
68
|
+
permissions: permissions, policies: policies,
|
|
69
|
+
memory_manager: memory_manager, trace_logger: trace_logger, approval_gate: approval_gate,
|
|
70
|
+
user_prompt: user_prompt
|
|
60
71
|
)
|
|
61
72
|
apply_agent_config(cfg)
|
|
62
|
-
@user_prompt = UserPrompt.new(stdin: cfg.stdin, stdout: cfg.stdout)
|
|
73
|
+
@user_prompt = cfg.user_prompt || UserPrompt.new(stdin: cfg.stdin, stdout: cfg.stdout)
|
|
63
74
|
@context_manager = Context::Manager.new(max_tokens: @max_tokens, context_summarize: @context_summarize)
|
|
64
75
|
@hooks = Streaming::Hooks.new
|
|
65
76
|
attach_audit_logger if resolved_audit_enabled
|
|
@@ -74,6 +85,38 @@ module OllamaAgent
|
|
|
74
85
|
execute_agent_turns(messages)
|
|
75
86
|
end
|
|
76
87
|
|
|
88
|
+
# Switch the chat model for subsequent {#run} calls (same session, same client).
|
|
89
|
+
# Accepts Ollama local tags (e.g. +llama3.2+) or cloud catalog names (e.g. +glm-5.1+).
|
|
90
|
+
#
|
|
91
|
+
# @param name [String]
|
|
92
|
+
# @return [String] the normalized model id
|
|
93
|
+
# @raise [OllamaAgent::Error] when +name+ is blank
|
|
94
|
+
def assign_chat_model!(name)
|
|
95
|
+
n = name.to_s.strip
|
|
96
|
+
raise Error, "Model name cannot be empty" if n.empty?
|
|
97
|
+
|
|
98
|
+
@model = n
|
|
99
|
+
n
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Names from the local Ollama daemon (+/api/tags+ on your +base_url+). Not used by the REPL +/models+ command.
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<String>]
|
|
105
|
+
def list_local_model_names
|
|
106
|
+
return [] unless @client.respond_to?(:list_model_names)
|
|
107
|
+
|
|
108
|
+
@client.list_model_names
|
|
109
|
+
rescue StandardError
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Cloud catalog from +https://ollama.com/api/tags+ (see {OllamaCloudCatalog}). REPL +/models+ uses this only.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<String>]
|
|
116
|
+
def list_cloud_model_names
|
|
117
|
+
OllamaCloudCatalog.list_model_names
|
|
118
|
+
end
|
|
119
|
+
|
|
77
120
|
private
|
|
78
121
|
|
|
79
122
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- maps AgentConfig to ivars + resolved max turns
|
|
@@ -100,20 +143,50 @@ module OllamaAgent
|
|
|
100
143
|
@context_summarize = cfg.context_summarize
|
|
101
144
|
strict = EnvConfig.strict_env?
|
|
102
145
|
@max_turns = EnvConfig.fetch_int("OLLAMA_AGENT_MAX_TURNS", MAX_TURNS, strict: strict)
|
|
146
|
+
# v2 platform subsystems (all optional; nil keeps legacy behaviour)
|
|
147
|
+
@budget = cfg.budget || Core::Budget.new(max_steps: @max_turns, max_tokens: @max_tokens)
|
|
148
|
+
@loop_detector = Core::LoopDetector.new
|
|
149
|
+
@trace_logger = cfg.trace_logger
|
|
150
|
+
@memory_manager = cfg.memory_manager
|
|
151
|
+
@permissions = cfg.permissions
|
|
152
|
+
@policies = cfg.policies
|
|
153
|
+
@approval_gate = cfg.approval_gate
|
|
154
|
+
@provider = cfg.provider
|
|
155
|
+
@provider_name = cfg.provider_name
|
|
103
156
|
end
|
|
104
157
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
105
158
|
|
|
106
159
|
# rubocop:disable Metrics/MethodLength -- turn loop with early break
|
|
107
160
|
def execute_agent_turns(messages)
|
|
108
161
|
@current_turn = 0
|
|
162
|
+
@budget.reset!
|
|
163
|
+
@loop_detector.reset!
|
|
164
|
+
@trace_logger&.start_run(query: messages.last&.fetch(:content, nil))
|
|
165
|
+
|
|
109
166
|
@max_turns.times do
|
|
110
167
|
@current_turn += 1
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
@budget.record_step!
|
|
169
|
+
|
|
170
|
+
if @budget.exceeded?
|
|
171
|
+
reason = @budget.exceeded_reason
|
|
172
|
+
warn "ollama_agent: budget exceeded — #{reason}"
|
|
173
|
+
@trace_logger&.budget_exceeded(reason: reason)
|
|
174
|
+
break
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
trimmed = trimmed_messages_for_chat(messages)
|
|
178
|
+
message = chat_assistant_message(trimmed)
|
|
113
179
|
tool_calls = tool_calls_from(message)
|
|
114
180
|
persist_assistant_turn(messages, message)
|
|
115
181
|
break if tool_calls.empty?
|
|
116
182
|
|
|
183
|
+
if @loop_detector.loop_detected?
|
|
184
|
+
summary = @loop_detector.loop_summary
|
|
185
|
+
warn "ollama_agent: #{summary}"
|
|
186
|
+
@trace_logger&.loop_detected(summary: summary)
|
|
187
|
+
break
|
|
188
|
+
end
|
|
189
|
+
|
|
117
190
|
append_tool_results(messages, tool_calls)
|
|
118
191
|
end
|
|
119
192
|
emit_turn_complete(messages)
|
|
@@ -206,6 +279,9 @@ module OllamaAgent
|
|
|
206
279
|
end
|
|
207
280
|
|
|
208
281
|
def announce_assistant_content(message)
|
|
282
|
+
@hooks.emit(:on_assistant_message, { message: message })
|
|
283
|
+
return if @hooks.subscribed?(:on_assistant_message)
|
|
284
|
+
|
|
209
285
|
Console.puts_assistant_message(message)
|
|
210
286
|
end
|
|
211
287
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require_relative "repl_shared"
|
|
6
|
+
|
|
7
|
+
module OllamaAgent
|
|
8
|
+
class CLI
|
|
9
|
+
# Interactive REPL for the agent.
|
|
10
|
+
#
|
|
11
|
+
# Features:
|
|
12
|
+
# - Slash commands (/help, /model, /models, /session, /memory, /status, /clear, /config, /provider, /index)
|
|
13
|
+
# - Readline history with persistent file
|
|
14
|
+
# - Multi-line input (end with blank line)
|
|
15
|
+
# - Session resume inside REPL
|
|
16
|
+
# - Token budget and loop-detector status display
|
|
17
|
+
# - Graceful Ctrl-C and Ctrl-D handling
|
|
18
|
+
# rubocop:disable Metrics/ClassLength -- readline wiring + banner + history
|
|
19
|
+
class Repl
|
|
20
|
+
include ReplShared
|
|
21
|
+
|
|
22
|
+
HISTORY_FILE = File.join(Dir.home, ".config", "ollama_agent", "repl_history")
|
|
23
|
+
MAX_HISTORY = 500
|
|
24
|
+
|
|
25
|
+
PROMPT = "\e[32mollama\e[0m \e[90m›\e[0m "
|
|
26
|
+
|
|
27
|
+
def initialize(agent:, memory: nil, budget: nil, stdout: $stdout, stderr: $stderr)
|
|
28
|
+
@agent = agent
|
|
29
|
+
@memory = memory
|
|
30
|
+
@budget = budget
|
|
31
|
+
@stdout = stdout
|
|
32
|
+
@stderr = stderr
|
|
33
|
+
@running = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# rubocop:disable Metrics/MethodLength -- readline loop + history ensure
|
|
37
|
+
def start
|
|
38
|
+
@running = true
|
|
39
|
+
setup_readline
|
|
40
|
+
print_banner
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
input = read_line
|
|
44
|
+
break if input.nil?
|
|
45
|
+
|
|
46
|
+
line = input.chomp.strip
|
|
47
|
+
next if line.empty?
|
|
48
|
+
|
|
49
|
+
break if %w[/exit exit].include?(line)
|
|
50
|
+
|
|
51
|
+
if line.start_with?("/")
|
|
52
|
+
handle_slash(line)
|
|
53
|
+
else
|
|
54
|
+
run_query(line)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@stdout.puts "\n\e[90mGoodbye.\e[0m"
|
|
59
|
+
ensure
|
|
60
|
+
save_history
|
|
61
|
+
end
|
|
62
|
+
# rubocop:enable Metrics/MethodLength
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def setup_readline
|
|
67
|
+
return unless readline_available?
|
|
68
|
+
|
|
69
|
+
load_history
|
|
70
|
+
Readline.completion_proc = slash_completer
|
|
71
|
+
configure_readline_slash_completion
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Shows command hints: type `/` then Tab (lists matches) or Tab again to cycle.
|
|
75
|
+
# With GNU Readline, `autocompletion` can show inline candidates while typing.
|
|
76
|
+
def configure_readline_slash_completion
|
|
77
|
+
Readline.completion_append_character = "" if Readline.respond_to?(:completion_append_character=)
|
|
78
|
+
Readline.autocompletion = true if Readline.respond_to?(:autocompletion=)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def slash_completer
|
|
82
|
+
proc do |input|
|
|
83
|
+
slash_completer_candidates.select { |cmd| cmd.start_with?(input.to_s) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_line
|
|
88
|
+
if readline_available?
|
|
89
|
+
Readline.readline(PROMPT, true)
|
|
90
|
+
else
|
|
91
|
+
@stdout.print PROMPT
|
|
92
|
+
@stdout.flush
|
|
93
|
+
$stdin.gets
|
|
94
|
+
end
|
|
95
|
+
rescue Interrupt
|
|
96
|
+
@stdout.puts ""
|
|
97
|
+
""
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def readline_available?
|
|
101
|
+
return @readline_available unless @readline_available.nil?
|
|
102
|
+
|
|
103
|
+
@readline_available = (require "readline") && true
|
|
104
|
+
rescue LoadError
|
|
105
|
+
@readline_available = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_query(query)
|
|
109
|
+
@agent.run(query)
|
|
110
|
+
rescue OllamaAgent::Error => e
|
|
111
|
+
@stderr.puts "\e[31mError: #{e.message}\e[0m"
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
@stderr.puts "\e[31m#{e.class}: #{e.message}\e[0m"
|
|
114
|
+
@stderr.puts e.backtrace.first(5).join("\n") if ENV["OLLAMA_AGENT_DEBUG"] == "1"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# rubocop:disable Metrics/MethodLength -- ASCII banner lines
|
|
118
|
+
def print_banner
|
|
119
|
+
@stdout.puts "\e[1m\e[34m"
|
|
120
|
+
@stdout.puts " ██████╗ ██╗ ██╗ █████╗ ███╗ ███╗ █████╗ "
|
|
121
|
+
@stdout.puts " ██╔═══██╗██║ ██║ ██╔══██╗████╗ ████║██╔══██╗"
|
|
122
|
+
@stdout.puts " ██║ ██║██║ ██║ ███████║██╔████╔██║███████║"
|
|
123
|
+
@stdout.puts " ██║ ██║██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║"
|
|
124
|
+
@stdout.puts " ╚██████╔╝███████╗███████╗██║ ██║██║ ╚═╝ ██║██║ ██║"
|
|
125
|
+
@stdout.puts " ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"
|
|
126
|
+
@stdout.puts "\e[0m"
|
|
127
|
+
@stdout.puts " Universal AI operator runtime • type \e[33m/help\e[0m for commands"
|
|
128
|
+
print_slash_completion_hint if readline_available?
|
|
129
|
+
@stdout.puts ""
|
|
130
|
+
end
|
|
131
|
+
# rubocop:enable Metrics/MethodLength
|
|
132
|
+
|
|
133
|
+
def print_slash_completion_hint
|
|
134
|
+
@stdout.puts " \e[33m/\e[0m then \e[33mTab\e[0m — slash-command hints; Tab again to list all matches"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_history
|
|
138
|
+
return unless File.exist?(HISTORY_FILE)
|
|
139
|
+
|
|
140
|
+
File.readlines(HISTORY_FILE, chomp: true).last(MAX_HISTORY).each do |line|
|
|
141
|
+
Readline::HISTORY.push(line)
|
|
142
|
+
end
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def save_history
|
|
148
|
+
return unless readline_available?
|
|
149
|
+
|
|
150
|
+
dir = File.dirname(HISTORY_FILE)
|
|
151
|
+
FileUtils.mkdir_p(dir)
|
|
152
|
+
File.write(HISTORY_FILE, Readline::HISTORY.to_a.last(MAX_HISTORY).join("\n"))
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
# rubocop:enable Metrics/ClassLength
|
|
158
|
+
end
|
|
159
|
+
end
|