ollama-client 1.3.0 → 1.4.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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +11 -0
  3. data/API_CONTRACT.md +79 -8
  4. data/CHANGELOG.md +45 -0
  5. data/CONTRIBUTING.md +8 -0
  6. data/README.md +205 -273
  7. data/ROADMAP.md +23 -0
  8. data/docs/API_GAPS.md +18 -141
  9. data/docs/ARCHITECTURE.md +141 -0
  10. data/docs/AREAS_FOR_CONSIDERATION.md +13 -1
  11. data/docs/CLOUD.md +19 -0
  12. data/docs/CONSOLE_IMPROVEMENTS.md +20 -1
  13. data/docs/ECOSYSTEM_GEMS.md +41 -0
  14. data/docs/ECOSYSTEM_STRATEGY.md +151 -0
  15. data/docs/GETTING_STARTED.md +16 -8
  16. data/docs/INTEGRATION_TESTING.md +23 -5
  17. data/docs/PRODUCTION_FIXES.md +15 -3
  18. data/docs/QUICK_START.md +1 -1
  19. data/docs/README.md +1 -0
  20. data/docs/RUBYLLM_ADOPTION_MATRIX.md +273 -0
  21. data/docs/adr/001-openai-boundary.md +12 -0
  22. data/docs/adr/002-transport-abstraction.md +12 -0
  23. data/docs/adr/003-response-normalization.md +12 -0
  24. data/docs/adr/004-mock-transport.md +16 -0
  25. data/docs/adr/005-error-taxonomy.md +16 -0
  26. data/docs/adr/006-stream-runtime.md +16 -0
  27. data/docs/ecosystem/BOUNDARIES.md +19 -0
  28. data/docs/ecosystem/DEPENDENCY_GRAPH.md +14 -0
  29. data/docs/ecosystem/DESIGN_PRINCIPLES.md +9 -0
  30. data/docs/ecosystem/EXISTING_REPOS.md +31 -0
  31. data/docs/ecosystem/EXPERIMENTAL_LABS.md +24 -0
  32. data/docs/ecosystem/OVERVIEW.md +28 -0
  33. data/docs/ecosystem/RELEASE_ORDER.md +17 -0
  34. data/docs/observability/README.md +8 -0
  35. data/docs/rails/README.md +7 -0
  36. data/docs/rfcs/0001-stream-runtime.md +6 -0
  37. data/docs/rfcs/0002-schema-system.md +6 -0
  38. data/docs/rfcs/0003-observability-hooks.md +6 -0
  39. data/docs/rfcs/0004-async-runtime.md +6 -0
  40. data/docs/rfcs/README.md +16 -0
  41. data/docs/runtime/ERROR_CONTRACT.md +14 -0
  42. data/docs/runtime/SCHEMA_CONTRACT.md +28 -0
  43. data/docs/runtime/STREAM_CONTRACT.md +17 -0
  44. data/docs/runtime/STREAM_RUNTIME.md +20 -0
  45. data/docs/runtime/TRANSPORT_CONTRACT.md +26 -0
  46. data/docs/schema/README.md +8 -0
  47. data/docs/schema/STRUCTURED_OUTPUTS.md +22 -0
  48. data/docs/streaming/README.md +8 -0
  49. data/docs/testing/README.md +7 -0
  50. data/docs/testing/REPLAY_SYSTEM.md +17 -0
  51. data/docs/transport/README.md +7 -0
  52. data/examples/README.md +44 -0
  53. data/examples/basic_chat.rb +9 -0
  54. data/examples/cloud_models.rb +162 -0
  55. data/examples/embeddings.rb +10 -0
  56. data/examples/free_catalog.json +290 -0
  57. data/examples/generate.rb +9 -0
  58. data/examples/streaming.rb +12 -0
  59. data/examples/structured_tools.rb +90 -0
  60. data/examples/tool_calling_direct.rb +101 -0
  61. data/examples/tool_dto_example.rb +94 -0
  62. data/exe/ollama-client +5 -1
  63. data/lib/ollama/agent/executor.rb +243 -0
  64. data/lib/ollama/agent/messages.rb +31 -0
  65. data/lib/ollama/agent/planner.rb +45 -0
  66. data/lib/ollama/api_key_pool.rb +61 -0
  67. data/lib/ollama/attachment.rb +74 -0
  68. data/lib/ollama/capabilities.rb +1 -1
  69. data/lib/ollama/chat_response.rb +33 -0
  70. data/lib/ollama/client/chat/request_preparer.rb +82 -0
  71. data/lib/ollama/client/chat.rb +58 -68
  72. data/lib/ollama/client/chat_stream_processor.rb +85 -25
  73. data/lib/ollama/client/generate/request_preparer.rb +118 -0
  74. data/lib/ollama/client/generate/response_formatter.rb +95 -0
  75. data/lib/ollama/client/generate.rb +83 -165
  76. data/lib/ollama/client/model_management.rb +182 -84
  77. data/lib/ollama/client/openai_compat.rb +185 -0
  78. data/lib/ollama/client/raw.rb +66 -0
  79. data/lib/ollama/client/tool_intent.rb +38 -0
  80. data/lib/ollama/client/web_search.rb +39 -0
  81. data/lib/ollama/client.rb +83 -35
  82. data/lib/ollama/config.rb +122 -17
  83. data/lib/ollama/embeddings.rb +67 -29
  84. data/lib/ollama/errors.rb +55 -1
  85. data/lib/ollama/events.rb +55 -0
  86. data/lib/ollama/generate_stream_handler.rb +23 -4
  87. data/lib/ollama/http_error_handler.rb +42 -0
  88. data/lib/ollama/messages.rb +109 -0
  89. data/lib/ollama/middleware/cache.rb +73 -0
  90. data/lib/ollama/middleware/logger.rb +74 -0
  91. data/lib/ollama/middleware/metrics.rb +84 -0
  92. data/lib/ollama/middleware/tracing.rb +101 -0
  93. data/lib/ollama/middleware.rb +39 -0
  94. data/lib/ollama/model_profile.rb +1 -1
  95. data/lib/ollama/openai.rb +16 -0
  96. data/lib/ollama/options.rb +63 -1
  97. data/lib/ollama/params.rb +139 -0
  98. data/lib/ollama/parsers/base.rb +24 -0
  99. data/lib/ollama/parsers/chat.rb +22 -0
  100. data/lib/ollama/parsers/embeddings.rb +23 -0
  101. data/lib/ollama/parsers/generate.rb +38 -0
  102. data/lib/ollama/parsers/list_running.rb +15 -0
  103. data/lib/ollama/parsers/show_model.rb +14 -0
  104. data/lib/ollama/parsers/version.rb +15 -0
  105. data/lib/ollama/pipeline.rb +169 -0
  106. data/lib/ollama/plugins.rb +83 -0
  107. data/lib/ollama/policies/auto_pull.rb +69 -0
  108. data/lib/ollama/policies/base.rb +70 -0
  109. data/lib/ollama/policies/capability_validation.rb +88 -0
  110. data/lib/ollama/policies/fallback.rb +57 -0
  111. data/lib/ollama/policies/rate_limit.rb +115 -0
  112. data/lib/ollama/policies/repair_json.rb +137 -0
  113. data/lib/ollama/policies/retry/strategies/exponential.rb +27 -0
  114. data/lib/ollama/policies/retry/strategies/fixed.rb +27 -0
  115. data/lib/ollama/policies/retry/strategies/jitter.rb +30 -0
  116. data/lib/ollama/policies/retry/strategies/linear.rb +27 -0
  117. data/lib/ollama/policies/retry.rb +152 -0
  118. data/lib/ollama/policies/schema_repair.rb +180 -0
  119. data/lib/ollama/policies/timeout.rb +43 -0
  120. data/lib/ollama/policies.rb +24 -0
  121. data/lib/ollama/prompt.rb +86 -0
  122. data/lib/ollama/prompt_adapters/base.rb +3 -2
  123. data/lib/ollama/prompt_adapters/gemma4.rb +22 -14
  124. data/lib/ollama/prompts/tool_planner.rb +35 -0
  125. data/lib/ollama/providers/base.rb +70 -0
  126. data/lib/ollama/providers/llama_cpp.rb +131 -0
  127. data/lib/ollama/providers/ollama.rb +54 -0
  128. data/lib/ollama/providers/openai.rb +134 -0
  129. data/lib/ollama/providers.rb +28 -0
  130. data/lib/ollama/rate_limit_handler.rb +48 -0
  131. data/lib/ollama/request.rb +169 -0
  132. data/lib/ollama/response.rb +3 -2
  133. data/lib/ollama/responses/base.rb +11 -0
  134. data/lib/ollama/responses/chat.rb +11 -0
  135. data/lib/ollama/responses/embeddings.rb +14 -0
  136. data/lib/ollama/responses/generate.rb +22 -0
  137. data/lib/ollama/schema_dsl.rb +97 -0
  138. data/lib/ollama/schema_validator.rb +90 -53
  139. data/lib/ollama/schemas/tool_intent.json +15 -0
  140. data/lib/ollama/schemas/tool_intent.rb +16 -0
  141. data/lib/ollama/serializers/base.rb +44 -0
  142. data/lib/ollama/serializers/chat.rb +42 -0
  143. data/lib/ollama/serializers/embeddings.rb +36 -0
  144. data/lib/ollama/serializers/generate.rb +47 -0
  145. data/lib/ollama/streaming_observer.rb +22 -0
  146. data/lib/ollama/testing.rb +103 -0
  147. data/lib/ollama/tool/function/parameters/property.rb +72 -0
  148. data/lib/ollama/tool/function/parameters.rb +101 -0
  149. data/lib/ollama/tool/function.rb +78 -0
  150. data/lib/ollama/tool.rb +60 -0
  151. data/lib/ollama/tool_dsl.rb +93 -0
  152. data/lib/ollama/tool_intent.rb +19 -0
  153. data/lib/ollama/transport/base.rb +42 -0
  154. data/lib/ollama/transport/mock.rb +48 -0
  155. data/lib/ollama/transport/net_http.rb +76 -0
  156. data/lib/ollama/transport/request.rb +20 -0
  157. data/lib/ollama/transport/response.rb +41 -0
  158. data/lib/ollama/transport.rb +26 -0
  159. data/lib/ollama/version.rb +1 -1
  160. data/lib/ollama_client.rb +35 -0
  161. data/script/live_branch_smoke/chat_exercises.rb +232 -0
  162. data/script/live_branch_smoke/generation_exercises.rb +128 -0
  163. data/script/live_branch_smoke/model_exercises.rb +105 -0
  164. data/script/live_branch_smoke/utility_exercises.rb +375 -0
  165. data/script/live_branch_smoke.rb +159 -174
  166. data/test_all_features.rb +315 -0
  167. metadata +158 -24
  168. data/.cursor/.gitignore +0 -1
  169. data/RELEASE_NOTES_v0.2.6.md +0 -41
  170. data/devagent_proper.rb +0 -430
  171. data/docs/TESTING.md +0 -508
  172. data/examples/agent_loop.rb +0 -120
  173. data/examples/failure_modes/invalid_json_repair.rb +0 -42
  174. data/examples/production/rails_agent.rb +0 -62
  175. data/market.jpg +0 -0
  176. data/print_capabilities.rb +0 -20
  177. data/schema.json +0 -1
  178. data/test_tool.rb +0 -26
@@ -0,0 +1,273 @@
1
+ # RubyLLM → ollama-client Adoption Matrix
2
+
3
+ **Purpose:** decide, feature by feature, what `ollama-client` should take from RubyLLM, what it should
4
+ take *and improve*, what it should refuse, and what belongs in an ecosystem gem — so the next build
5
+ phase is driven by a decision, not by opportunity.
6
+
7
+ **Positioning premise:** RubyLLM is breadth (one interface across ~12 providers). `ollama-client` is
8
+ depth (the complete Ruby runtime for one provider). Adoption is therefore about **ergonomics parity**,
9
+ not **scope parity**. Anything RubyLLM does *because* it must abstract many providers is a candidate
10
+ for rejection.
11
+
12
+ **Sources**
13
+
14
+ - RubyLLM public API: [rubyllm.com](https://rubyllm.com/) — [chat](https://rubyllm.com/chat/),
15
+ [tools](https://rubyllm.com/tools/), [models](https://rubyllm.com/models/),
16
+ [streaming](https://rubyllm.com/streaming/), [rails](https://rubyllm.com/rails/),
17
+ [configuration](https://rubyllm.com/configuration/) (surface as of RubyLLM ~1.15, incl. `RubyLLM::Agent`).
18
+ - `ollama-client` current state: this repository at `v1.3.0` (`lib/ollama/version.rb`), ~5.8k LOC in `lib/`,
19
+ public surface frozen by [`API_CONTRACT.md`](../API_CONTRACT.md).
20
+
21
+ ---
22
+
23
+ ## Verdict legend
24
+
25
+ | Verdict | Meaning |
26
+ |---|---|
27
+ | **COPY** | Adopt the concept close to as-is. RubyLLM's shape is right and provider-neutral. |
28
+ | **IMPROVE** | Adopt the concept, but ship a strictly better version using Ollama-specific depth. |
29
+ | **REJECT** | Do not build. Either it is a multi-provider artifact, or Ollama has no such capability. |
30
+ | **ECOSYSTEM** | Right idea, wrong gem. Belongs in `ollama-rails` / `ollama-agent` / `ollama-testing` / `ollama-observability`. |
31
+ | **HAVE** | Already exists here, at parity or better. No work beyond docs. |
32
+
33
+ ---
34
+
35
+ ## A. Chat & conversation
36
+
37
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
38
+ |---|---|---|---|---|---|
39
+ | A1 | Stateful conversation object | `chat = RubyLLM.chat(model:)`; `chat.ask("…")` twice keeps history | **None.** `chat` is a stateless one-shot on `Client` (`lib/ollama/client/chat.rb:29`) returning `Ollama::Response`. Callers hand-manage `messages:` arrays | **COPY** | `Ollama::Chat` — `client.chat(model:)` with no `messages:` returns a `Chat`; `chat.ask` appends and returns `Ollama::Response`. Highest-leverage single item in this document |
40
+ | A2 | Backwards-compatible entry point | n/a (RubyLLM never had a hash API) | `chat(messages:, …)` is contract-frozen until v2.0 | **IMPROVE** | Overload, don't replace: `messages:` present → today's exact behavior; `messages:` absent → `Ollama::Chat`. Zero contract breakage |
41
+ | A3 | System prompt helper | `with_instructions(text, append: false)` | Only `Ollama::Messages#system` (`lib/ollama/messages.rb:29`) | **COPY** | `chat.with_instructions` delegating to `Messages#system` (which already does replace-or-append) |
42
+ | A4 | Fluent config builders | `with_model`, `with_temperature`, `with_params`, `with_headers` | Per-call kwargs + `Ollama::Options` + per-client `Config` | **IMPROVE** | Add the fluent set *plus* Ollama-native builders RubyLLM cannot have: `with_think(:high)`, `with_keep_alive("30m")`, `with_num_ctx(32_768)`, `with_options(Ollama::Options)` |
43
+ | A5 | Conversation history access | `messages`, `add_message`, `reset_messages!` | `Ollama::Messages` builder exists but is not attached to a conversation | **COPY** | `chat.messages` returns the `Messages` builder; `add_message`, `reset!` |
44
+ | A6 | History hygiene across turns | Not addressed — RubyLLM replays raw history | `HistorySanitizer` + `ModelProfile#history_policy` (`:exclude_thoughts`) already exist (`lib/ollama/history_sanitizer.rb`, `lib/ollama/model_profile.rb:123`) | **HAVE / exceed** | Wire the existing sanitizer into `Ollama::Chat` by default. This is depth RubyLLM structurally cannot match — it does not know that Gemma-4 thinking blocks must be dropped from replayed history |
45
+ | A7 | Message/response object | `response.content`, `.model_id`, `.raw`, `.tokens.{input,output,cache_read,thinking}`, `.cost.total` | `Ollama::Response` with `content`, `usage`, `latency_ms`, `total_duration`, `eval_count`, `logprobs` (`lib/ollama/response.rb`) | **IMPROVE** | Keep `usage`. **Reject `cost`** (local inference has no per-token price) and spend that slot on `tokens_per_second`, `load_duration_ms`, `prompt_eval_rate` — the numbers that actually matter locally |
46
+ | A8 | Lifecycle callbacks | `before_message`, `after_message`, `before_tool_call`, `after_tool_result` (legacy `on_*` deprecated) | `hooks: {on_token:, on_thought:, on_tool_call:, on_error:, on_complete:}` (contract-frozen) + separate `StreamEvent` struct + separate `StreamingObserver` | **IMPROVE** | Do **not** add a third callback system. Collapse to one typed `Ollama::StreamEvent` bus (`lib/ollama/stream_event.rb` already defines the taxonomy), with `hooks:` retained as the frozen adapter over it |
47
+ | A9 | Prompt caching | `cache_prompts`, cache token accounting | n/a — Ollama has no prompt-cache billing surface | **REJECT** | Ollama's equivalent lever is `keep_alive` + preload, already covered by A4/E5 |
48
+ | A10 | Reusable assistant preset | `class X < RubyLLM::Agent; model …; instructions …; tools … end` | `Ollama::Agent::Executor`, `Ollama::Agent::Planner` in core (`lib/ollama/agent/`) | **ECOSYSTEM** | Agent presets → `ollama-agent`. Core ships `Ollama::Chat` only. See §L for the executor question |
49
+
50
+ ---
51
+
52
+ ## B. Tools
53
+
54
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
55
+ |---|---|---|---|---|---|
56
+ | B1 | Class-based tool definition | `class X < RubyLLM::Tool; description …; param …; def execute(**) end` | `Ollama::ToolDSL` with `description`, `tool_name`, `input do … end`, `output do … end`, `call { }` (`lib/ollama/tool_dsl.rb`) | **IMPROVE** | Keep the schema DSL (it is better — typed `input`/`output` blocks). Replace the confusing `call { }` "constructor" block with a plain `#execute(**kwargs)` instance method. `to_tool_hash` stays |
57
+ | B2 | Nested/complex params | `params do object :w do … end; array :p, of: :string end` | `SchemaDSL` has `object`/`array` but `object` takes a raw `properties:` hash — no nested block (`lib/ollama/schema_dsl.rb:61`) | **COPY** | Add block form to `SchemaDSL#object` and `of:` to `#array`. Small change, removes the last reason to hand-write JSON Schema |
58
+ | B3 | Signature inference | v1.15+: builds schema from `execute` kwargs when no `param` given | Exists but buried in `Agent::Executor#infer_parameters` and types everything as `"string"` | **IMPROVE** | Promote inference to `ToolDSL`, and infer types from Ruby defaults/`Data` members rather than defaulting to string |
59
+ | B4 | Attaching tools | `with_tool(T)`, `with_tools(A, B)` | `chat(tools: [T.to_tool_hash])` — user must serialize manually | **COPY** | `chat.with_tool(T)` accepting a `ToolDSL` subclass, `Ollama::Tool` DTO, or raw hash |
60
+ | B5 | Tool call parsing | Provider-normalized | `Response::Message::ToolCall` + `Function#arguments` with JSON-string fallback (`lib/ollama/response.rb:165`) — plus `ToolIntent` fallback for models that emit tool calls as prose | **HAVE / exceed** | Nothing. `lib/ollama/client/tool_intent.rb` has no RubyLLM equivalent |
61
+ | B6 | Halt the loop | `halt "…"` skips the model's post-tool commentary | None | **COPY** | Cheap, high value once a loop exists (§L) |
62
+ | B7 | Tool choice / call count | `choice: :auto\|:required\|:none\|Tool`, `calls: :many\|:one` | None | **PARTIAL / verify** | `/api/chat` does not expose `tool_choice`; the OpenAI-compat endpoint does. Ship `choice:` only where the provider supports it (`lib/ollama/providers/openai.rb`), and emulate `:none` by omitting `tools:`. Do not fake `:required` |
63
+ | B8 | Concurrent tool execution | `concurrency: :threads \| :fibers` | None | **ECOSYSTEM** | Scheduling policy → `ollama-agent` |
64
+ | B9 | Automatic execution loop | Core runs the tool loop inside `ask` | `Agent::Executor` (in core, undocumented in `API_CONTRACT.md`) | **CONTESTED** | See §L — this is the one boundary decision that must be made before A1 ships |
65
+ | B10 | Argument safety | Docs-only guidance ("treat as untrusted") | `Executor` does keyword/positional coercion + alias guessing (`directory`→`path`) | **IMPROVE** | Validate tool arguments against the declared `input` schema *before* dispatch using the existing `SchemaValidator`, and drop the alias guessing — it is a silent-wrong-behavior generator |
66
+
67
+ ---
68
+
69
+ ## C. Structured output
70
+
71
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
72
+ |---|---|---|---|---|---|
73
+ | C1 | Schema class DSL | `class S < RubyLLM::Schema; string :name; number :price; array :f do string end; end` | `Ollama::SchemaDSL.define { … }.to_h` — block/instance form, not a subclassable class | **COPY** | `class TradeSignal < Ollama::Schema` with the same field methods. Keep `SchemaDSL.define` as the anonymous form |
74
+ | C2 | Attaching a schema | `chat.with_schema(S)` → `response.content` is a parsed Hash | `generate(schema:)` → Hash; `chat(format:)` → raw String the caller must `JSON.parse` | **IMPROVE** | `chat.with_schema(S)`. Non-negotiable per `CLAUDE.md`: `generate` keeps its String-vs-Hash rule unchanged; the new typed path lives on `Chat` |
75
+ | C3 | Validation & repair | Provider-side JSON mode; no repair loop | `strict_json` + `SchemaValidator` + repair-prompt retry — **but only on `generate`**. `chat(format:)` bypasses validation and repair entirely | **IMPROVE** | **Real defect, not just a gap.** Route `chat(format:)`/`with_schema` through the same validate→repair→retry pipeline. Closing this is worth more than any DX item in this table |
76
+ | C4 | Typed result objects | Returns a Hash | Returns a Hash | **IMPROVE / differentiate** | `signal.direction` not `hash["direction"]`. Schema class instantiates a typed object with coercion. This is the flagship differentiator called out in the strategy |
77
+ | C5 | Incremental / streaming parse | None | `JsonFragmentExtractor` extracts balanced JSON from partial text (`lib/ollama/json_fragment_extractor.rb`) | **HAVE → extend** | Feed the extractor from the stream so partial structured output can render live. RubyLLM has no answer here |
78
+ | C6 | Enum / constraint coverage | `string :x, enum: […]` | `enum`, `minimum`, `maximum`, `description`, `default`, `optional` | **HAVE** | Add `pattern`, `format`, `minItems` for completeness |
79
+
80
+ ---
81
+
82
+ ## D. Streaming
83
+
84
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
85
+ |---|---|---|---|---|---|
86
+ | D1 | Block streaming | `chat.ask("…") { \|chunk\| print chunk.content }` | `hooks: {on_token:}` — works, but the block form is what people expect | **COPY** | `chat.ask(…) { \|chunk\| }`; presence of a block implies `stream: true`, exactly as `hooks:` does today (`lib/ollama/client/chat.rb:74`) |
87
+ | D2 | Chunk object | `RubyLLM::Chunk` with `content`, `role`, `model_id`, `tool_calls`, `tokens`, `thinking` | `StreamEvent(type:, data:, model:)` with `thought?`/`answer?`/`tool_call?`/`terminal?` predicates and `to_jsonl` | **HAVE / exceed** | Typed event *taxonomy* beats an untyped chunk — reasoning is separated from answer at the type level. Just expose it publicly |
88
+ | D3 | Final message after streaming | `ask` returns the complete message even when streaming | `chat` returns assembled `Ollama::Response` via `ChatStreamProcessor` | **HAVE** | — |
89
+ | D4 | Stream cancellation | **None documented** | None | **IMPROVE / exceed** | `stream = chat.stream("…")`; `stream.cancel`, `stream.closed?`, `stream.metrics`, `stream.each_chunk`. This is a clear "exceed RubyLLM" slot and is already scoped in `docs/rfcs/0001-stream-runtime.md` |
90
+ | D5 | Streaming errors | Raises `RubyLLM::Error` subclasses mid-stream | `Ollama::StreamError` from `{"error":…}` NDJSON lines, `on_error` hook | **HAVE** | — |
91
+ | D6 | Turbo/ActionCable bridge | Documented Rails pattern | None | **ECOSYSTEM** | `ollama-rails` |
92
+ | D7 | Async / fibers | Works under `Async`; concurrency options in tools | None; `docs/rfcs/0004-async-runtime.md` drafted | **ECOSYSTEM (later core)** | Keep `Net::HTTP` core; async runtime as `ollama-stream` or an opt-in transport adapter — the `Transport::Base` boundary already supports this |
93
+
94
+ ---
95
+
96
+ ## E. Models & runtime — *the differentiation zone*
97
+
98
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
99
+ |---|---|---|---|---|---|
100
+ | E1 | Model registry object | `RubyLLM.models` → collection | `list_models` → `Array<Hash>` (`lib/ollama/client/model_management.rb:233`) | **IMPROVE** | `client.models` → `Ollama::ModelCollection`; `client.model("qwen3:8b")` → `Ollama::Model` |
101
+ | E2 | Registry filters | `all`, `chat_models`, `embedding_models`, `by_provider`, `by_family`, `find` | None (raw array) | **COPY** | `.running`, `.embedding`, `.vision`, `.tools`, `.thinking`, `.by_family`, `.find` — filters keyed off `Capabilities` + `ModelProfile`, not a static catalog |
102
+ | E3 | Model metadata source | **Static catalog** refreshed from models.dev + `refresh!` + `save_to_json` | Live `/api/tags` + `/api/show`, with `Capabilities.for` inference (`lib/ollama/capabilities.rb`) | **IMPROVE / exceed** | Live-first is the whole thesis. `model.capabilities` should prefer the server's own `/api/show` capability list and fall back to name/family heuristics — today `Capabilities.for` is heuristics-only |
103
+ | E4 | Model attributes | `context_window`, `max_tokens`, `supports_vision?`, `supports_functions?`, `family`, pricing | `ModelProfile` (static family table, `lib/ollama/model_profile.rb:14`) + capability hash | **IMPROVE** | `Ollama::Model#name/family/parameters/quantization/context_window/capabilities` — sourced from `/api/show` `model_info`, which carries real per-model context length and quantization. Static tables become fallback only |
104
+ | E5 | Runtime state | **Impossible for RubyLLM** | `list_running`/`ps`, `load_model`, `unload_model` exist but return hashes | **IMPROVE / exceed** | `model.running?`, `.loaded_at`, `.expires_at`, `.vram`, `.memory`, `.load`, `.unload`, `.performance.tokens_per_second`. This is the `/models` command in the AI Runtime Shell, and no multi-provider SDK can offer it |
105
+ | E6 | Lifecycle operations | `refresh!` only | `pull`, `push_model`, `delete_model`, `copy_model`, `create_model`, `blob_exists?`, `create_blob` | **HAVE / exceed** | Re-expose as instance methods on `Ollama::Model` (`model.pull`, `model.copy("prod")`) — same HTTP calls, better surface |
106
+ | E7 | Pricing / cost tracking | `input_price_per_million`, `cost.total` | None | **REJECT** | Meaningless locally. Replace with the E5 performance surface |
107
+ | E8 | Alias resolution | `aliases.json` maps `claude-sonnet` → versioned id | None — `qwen3` vs `qwen3:8b` is the caller's problem | **COPY (small)** | Resolve bare names against installed tags; raise a typed error listing near-matches instead of a bare 404 |
108
+ | E9 | Unknown-model escape | `assume_model_exists: true` | 404 → auto-pull → retry (contract-guaranteed) | **HAVE / exceed** | — |
109
+
110
+ ---
111
+
112
+ ## F. Other modalities
113
+
114
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
115
+ |---|---|---|---|---|---|
116
+ | F1 | Embeddings | `RubyLLM.embed(text)` → `Embedding` with `vectors`, `model`, `input_tokens` | `client.embeddings.embed(model:, input:)` → raw `Array<Float>`; `model:` is required | **COPY** | `client.embed("text")` using a new `config.default_embedding_model`; return an `Ollama::Embedding` object (`vectors`, `model`, `dimensions`, `usage`) that still `to_ary`s to the raw vector for compatibility |
117
+ | F2 | Attachments | `chat.ask("what is this?", with: "image.png")` — auto-detects type | `Ollama::Attachment.file/base64/url` + `MultimodalInput` + `inputs:` kwarg | **COPY (ergonomics only)** | Add `with:` sugar over the existing typed machinery. Keep `inputs:` as the explicit form |
118
+ | F3 | Image generation | `RubyLLM.paint` | None | **REJECT** | Ollama does not generate images |
119
+ | F4 | Transcription | `RubyLLM.transcribe` | None | **REJECT** | Not an Ollama endpoint |
120
+ | F5 | Moderation | `RubyLLM.moderate` | None | **REJECT** | Not an Ollama endpoint. A safety-model convention could live in `ollama-agent` |
121
+ | F6 | Thinking / reasoning | Surfaced as a `thinking` chunk field only | `think: true\|"high"\|"medium"\|"low"`, per-family `think_trigger` (`:flag` vs `:system_prompt_tag`), separated reasoning stream, `UnsupportedThinkingModel` | **HAVE / exceed** | Document it as a headline feature. `PromptAdapters` handling Gemma-4's tag-based thinking is genuinely unmatched |
122
+
123
+ ---
124
+
125
+ ## G. Configuration, transport, errors
126
+
127
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
128
+ |---|---|---|---|---|---|
129
+ | G1 | Global config | `RubyLLM.configure { \|c\| … }` | `OllamaClient.config` + per-client `Config` (per-client is the documented default) | **HAVE** | Keep per-client primacy — `CLAUDE.md` forbids mutating global config with live clients. Thread-safety here is already better |
130
+ | G2 | Isolated contexts | `RubyLLM.context { \|c\| … }` → independent, thread-safe | `Config.new` + `Client.new(config:)` — the same thing, less ceremony | **HAVE** | Documentation only |
131
+ | G3 | Retry tuning | `max_retries`, `retry_interval`, `retry_backoff_factor`, `retry_interval_randomness` | `config.retries` only; backoff hardcoded `2 ** attempt`, no jitter | **COPY** | Add interval/factor/jitter knobs. Contract-safe: `API_CONTRACT.md` explicitly permits retry-timing changes and new config attributes with compatible defaults |
132
+ | G4 | Logging | `logger`, `log_file`, `log_level`, `log_stream_debug` | `config.on_response` callback only | **COPY (core) + ECOSYSTEM** | Minimal `config.logger` in core; OTel/metrics/exporters → `ollama-observability` |
133
+ | G5 | Proxy / connection | `http_proxy`, Faraday connection options | `http_connection_options` (ssl + timeouts) | **COPY** | Proxy support + connection pooling (already on `ROADMAP.md`) |
134
+ | G6 | Transport swap | Faraday middleware, not a documented public seam | `Transport::Base`/`NetHTTP`/`Mock` factory (`lib/ollama/transport.rb`) + `client.use(policy)` middleware stack (`lib/ollama/policies/`) | **HAVE / exceed** | Formalize the middleware stack in `API_CONTRACT.md` |
135
+ | G7 | Raw escape hatch | `with_params`, `response.raw` | `client.raw.get/post/delete` (`lib/ollama/client/raw.rb`) + `client.openai` facade | **HAVE / exceed** | — |
136
+ | G8 | Error taxonomy | `Error`, `BadRequest`, `Unauthorized`, `RateLimit`, `ServerError`, `ModelNotFound`, … | 11 typed errors with documented retryability + recovery guarantees | **HAVE / exceed** | — |
137
+ | G9 | Multi-key failover | None | `ApiKeyPool` + round-robin + rate-limit rotation (`lib/ollama/api_key_pool.rb`) | **HAVE / exceed** | Relevant to Ollama Cloud; keep and document |
138
+
139
+ ---
140
+
141
+ ## H. Rails & test/observability tooling
142
+
143
+ | # | RubyLLM capability | RubyLLM API | ollama-client today | Verdict | Target |
144
+ |---|---|---|---|---|---|
145
+ | H1 | Persistence | `acts_as_chat`, `acts_as_message`, `acts_as_tool_call`, `acts_as_model` | None | **ECOSYSTEM** | `ollama-rails`. Hard rule stands: core imports nothing from Rails |
146
+ | H2 | Generators | `ruby_llm:install`, `:chat_ui`, `:load_models` | None | **ECOSYSTEM** | `ollama-rails` |
147
+ | H3 | Empty-assistant-message persistence pattern | Core Rails behavior; documented as blocking `validates :content, presence: true` | n/a | **REJECT the pattern** | Do not inherit this design flaw. `ollama-rails` should persist on completion, or persist a distinct `pending` state |
148
+ | H4 | Background jobs / Turbo | Documented patterns | None | **ECOSYSTEM** | `ollama-rails` |
149
+ | H5 | Deterministic test doubles | VCR in RubyLLM's own suite; no shipped public test API | `Transport::Mock` shipped in-gem; `docs/testing/REPLAY_SYSTEM.md` drafted | **HAVE → ECOSYSTEM** | `Ollama::Testing` helpers (`stub_chat`, `stub_stream`, `stub_error`) per `IMPROVE.md` Phase 5, then extract to `ollama-testing`. A shipped mock transport is a real advantage — RubyLLM users reach for WebMock themselves |
150
+ | H6 | Observability | Logging only | `on_response` hook, `Response#usage`, `#latency_ms` | **ECOSYSTEM** | `ollama-observability` (OTel spans, token metrics) |
151
+
152
+ ---
153
+
154
+ ## I. Where ollama-client already exceeds RubyLLM
155
+
156
+ No adoption work — these are the marketing surface and should be lifted into the README's lead section:
157
+
158
+ 1. **Thinking/reasoning as a first-class, per-family concern** — `think:` levels, `think_trigger` (API flag vs. system-prompt tag), reasoning separated from answer in the stream, `UnsupportedThinkingModel`.
159
+ 2. **Prompt adapters per model family** — Gemma-4/Qwen/DeepSeek/generic (`lib/ollama/prompt_adapters/`). RubyLLM cannot do this without becoming Ollama-specific.
160
+ 3. **History sanitization policies** — dropping thoughts from replayed history is a correctness issue in multi-turn agent loops that RubyLLM leaves to the user.
161
+ 4. **Structured-output repair loop** — validate → repair-prompt → retry, contract-guaranteed. RubyLLM trusts provider JSON mode.
162
+ 5. **Tool-intent recovery** — parsing tool calls from models that emit them as prose.
163
+ 6. **Auto-pull on 404** — a guaranteed recovery behavior that only makes sense for a local runtime.
164
+ 7. **Model lifecycle** — `ps`, `load`/`unload`, `keep_alive`, `create`/`copy`/`push`/blobs.
165
+ 8. **Shipped mock transport** and an explicit `API_CONTRACT.md`.
166
+ 9. **Multi-key failover** for Ollama Cloud.
167
+ 10. **Typed stream event taxonomy** with JSONL tracing built in.
168
+
169
+ ## J. Where RubyLLM is straightforwardly ahead
170
+
171
+ Stated plainly, because the matrix above is otherwise easy to read as self-congratulation:
172
+
173
+ 1. **No conversation object.** `chat.ask` twice is the single most expected thing in a modern LLM SDK, and it does not exist here (A1).
174
+ 2. **Tools do not execute.** `ToolDSL` produces a schema; nothing wires it to a callable in the public API (B1/B4/B9).
175
+ 3. **Structured output on `chat` is unvalidated** and returns a raw string (C2/C3).
176
+ 4. **No streaming block form** — hooks are more powerful and less familiar (D1).
177
+ 5. **Models are hashes**, not objects (E1).
178
+ 6. **Embeddings require an explicit `model:`** every call and return bare arrays (F1).
179
+ 7. **No Rails story at all** (H1–H4).
180
+
181
+ Items 1–6 are all fixable additively, without touching a frozen signature.
182
+
183
+ ## K. Ollama API coverage gaps (independent of RubyLLM)
184
+
185
+ Depth positioning means "100% Ollama API coverage" must be literally true. Open items to verify against
186
+ the current Ollama API docs before claiming it:
187
+
188
+ - ~~`web_search` / `web_fetch` cloud endpoints~~ — **done**: `client.web_search(query:, max_results:)` and `client.web_fetch(url:)` (`lib/ollama/client/web_search.rb`, documented in `API_CONTRACT.md`).
189
+ - `/api/embeddings` (legacy singular) alongside `/api/embed` — currently only the plural path is used. Low priority: the singular route is Ollama's deprecated legacy alias and `/api/embed` is the documented current endpoint.
190
+ - `/api/generate` `context` parameter (deprecated but present), `suffix` (FIM) — confirmed: `context`, `suffix`, and `raw` are all threaded through `Client::Generate` and `Params::Generate`.
191
+ - ~~Streamed `create`/`pull`/`push` progress: `hooks[:on_progress]` exists but is not in `API_CONTRACT.md`~~ — **done**: documented. (`create_model` itself still has no streaming hook — only `pull`/`push_model` do.)
192
+ - `/api/show` `model_info` fields (real context length, quantization, parameter count) are fetched but discarded — they are the raw material for E4. Still open; tracked as roadmap item E4, not a missing-endpoint gap.
193
+
194
+ ## L. The one contested boundary: who runs the tool loop?
195
+
196
+ The strategy says `ollama-client` owns tool *schema, parsing, validation, normalization*, and
197
+ `ollama-agent` owns *execution loops, permissions, scheduling, policies*. RubyLLM puts the loop in core.
198
+ Meanwhile `Ollama::Agent::Executor` already sits in this gem's `lib/`, undeclared in `API_CONTRACT.md`.
199
+ **UPDATE (2026-08): `Ollama::Agent::Executor` is now declared public — it is required by the default
200
+ load path, `#run` uses the current `chat()` API (tool loop included), and string or symbol tool keys
201
+ are both accepted. The strategic question below still stands for future `ollama-agent` work, but the
202
+ loop-in-core default is now implemented.**
203
+
204
+ **Recommendation: put a bounded loop in core, and keep policy out.**
205
+
206
+ Core (`ollama-client`) ships:
207
+
208
+ - `chat.with_tool(T)` and `chat.ask(…)` resolving tool calls automatically up to `max_tool_rounds:` (default small, e.g. 5), raising a typed `Ollama::ToolLoopExhausted` past that.
209
+ - Synchronous, in-order execution. Argument validation against the declared schema. `halt` support.
210
+
211
+ `ollama-agent` owns: permissions and sandboxing, concurrency/scheduling, retries and replanning,
212
+ memory, multi-agent orchestration, human-in-the-loop approval, budget enforcement.
213
+
214
+ Rationale: without execution, `with_tool` is a lie — the user still writes the loop, and the "most Ruby
215
+ developers don't need `ollama-agent`" goal fails on the most common use case. A bounded, policy-free
216
+ loop is a *protocol completion*, not an agent framework. `Agent::Executor` then either becomes the
217
+ private implementation of that loop or moves out to `ollama-agent` at the same time — it should not
218
+ remain a public-ish class in core with no contract.
219
+
220
+ ---
221
+
222
+ ## M. Prioritized build order
223
+
224
+ Derived strictly from the matrix. Every P0/P1 item is additive; none touches a signature frozen by
225
+ `API_CONTRACT.md`.
226
+
227
+ ### P0 — Correctness (ship before any ergonomics)
228
+
229
+ | Item | Ref | Why first |
230
+ |---|---|---|
231
+ | Route `chat(format:)` through validate → repair → retry | C3 | Two structured-output paths with different reliability guarantees is a defect, and the gem's core claim is deterministic structured output |
232
+ | Prefer `/api/show` server capabilities over name heuristics | E3 | `Capabilities.for` currently guesses from model names; the server knows |
233
+ | Decide the tool-loop boundary and document it | L | Blocks P1 tool work and `Agent::Executor`'s fate |
234
+
235
+ ### P1 — Ergonomics parity (the RubyLLM lesson)
236
+
237
+ | Item | Ref |
238
+ |---|---|
239
+ | `Ollama::Chat` stateful conversation + `ask` (+ history sanitizer wired in) | A1, A2, A5, A6 |
240
+ | `with_instructions` / `with_model` / `with_temperature` / `with_think` / `with_keep_alive` | A3, A4 |
241
+ | Block streaming `chat.ask(…) { \|chunk\| }` over the existing NDJSON path | D1 |
242
+ | `class T < Ollama::Tool` with `#execute`; `chat.with_tool(T)`; bounded loop; `halt` | B1, B4, B6, B9 |
243
+ | `class S < Ollama::Schema`; `chat.with_schema(S)` → typed object | C1, C2, C4 |
244
+ | `client.embed("text")` + `default_embedding_model` + `Ollama::Embedding` | F1 |
245
+ | `ask(…, with: "image.png")` sugar | F2 |
246
+
247
+ ### P2 — Depth that exceeds RubyLLM
248
+
249
+ | Item | Ref |
250
+ |---|---|
251
+ | `Ollama::Model` + `Ollama::ModelCollection` with live filters and lifecycle methods | E1, E2, E4, E6 |
252
+ | Runtime introspection: `running?`, `expires_at`, `vram`, `performance.tokens_per_second` | E5 |
253
+ | `Ollama::Stream` with `cancel`, `closed?`, `metrics`, `each_chunk` | D4 |
254
+ | Incremental structured-output parsing off the stream | C5 |
255
+ | Nested schema blocks, tool signature inference, schema-validated tool arguments | B2, B3, B10 |
256
+ | Retry/jitter knobs, `config.logger`, proxy support, model alias resolution | G3, G4, G5, E8 |
257
+
258
+ ### P3 — Ecosystem extraction
259
+
260
+ `ollama-testing` (H5) → `ollama-rails` (H1–H4) → `ollama-observability` (H6) → `ollama-agent` boundary
261
+ finalized (A10, B8, L) → `ollama-stream` / async (D7).
262
+
263
+ ## N. API contract impact
264
+
265
+ | Change class | Contract effect |
266
+ |---|---|
267
+ | `Ollama::Chat`, `Ollama::Model`, `Ollama::Schema`, `Ollama::Embedding`, `Ollama::Stream` | New classes — additive, no impact |
268
+ | `client.chat` with no `messages:` returning a `Chat` | New arity path; `messages:`-present behavior byte-identical. Permitted ("new optional keyword arguments") — but call it out explicitly in the contract |
269
+ | `chat(format:)` gaining validation/repair | **Behavior change.** Today it returns unvalidated content; after, it can raise `SchemaViolationError`. Requires a minor-version note, and arguably a `strict:` opt-out on `chat` to mirror `generate` |
270
+ | `Ollama::Response#cost` | Never — explicitly rejected (E7) |
271
+ | `config.retry_interval`, `config.logger`, `config.default_embedding_model`, `config.http_proxy` | Additive with compatible defaults — permitted |
272
+ | `Ollama::ToolLoopExhausted` | New error under the existing hierarchy — permitted |
273
+ | `Agent::Executor` | Must be either declared public with a contract, made private, or moved to `ollama-agent`. Currently undefined status. **UPDATE (2026-08): declared public and wired into the default load path; `#run` now uses the current `chat()` API.** |
@@ -0,0 +1,12 @@
1
+ # ADR-001: OpenAI Compatibility Boundary
2
+
3
+ ## Context
4
+ OpenAI request/response semantics can contaminate Ollama-native runtime APIs.
5
+
6
+ ## Decision
7
+ OpenAI compatibility is optional and loaded separately.
8
+
9
+ ## Consequences
10
+ - cleaner core semantics
11
+ - better long-term extensibility
12
+ - easier extraction to dedicated gem
@@ -0,0 +1,12 @@
1
+ # ADR-002: Transport Abstraction
2
+
3
+ ## Context
4
+ Direct HTTP coupling prevents adapter evolution and deterministic testing.
5
+
6
+ ## Decision
7
+ All runtime HTTP execution goes through transport adapters.
8
+
9
+ ## Consequences
10
+ - adapter extensibility (`:net_http`, `:mock`, future async)
11
+ - cleaner request pipeline layering
12
+ - improved testability
@@ -0,0 +1,12 @@
1
+ # ADR-003: Normalized Transport Response
2
+
3
+ ## Context
4
+ Runtime logic was coupled to backend-specific response objects.
5
+
6
+ ## Decision
7
+ Introduce transport response object with normalized fields.
8
+
9
+ ## Consequences
10
+ - transport-neutral policies and hooks
11
+ - easier observability
12
+ - incremental migration with compatibility shims
@@ -0,0 +1,16 @@
1
+ # ADR-004: Mock Transport
2
+
3
+ ## Context
4
+ Deterministic CI and failure simulation require decoupling tests from live Ollama servers.
5
+
6
+ ## Decision
7
+ Introduce a `:mock` transport adapter in core transport architecture.
8
+
9
+ ## Alternatives rejected
10
+ - Global monkeypatching HTTP
11
+ - Ad-hoc stubs per test
12
+
13
+ ## Consequences
14
+ - deterministic request/response tests
15
+ - foundation for replay/latency/failure simulation
16
+ - enables test tooling without runtime coupling
@@ -0,0 +1,16 @@
1
+ # ADR-005: Typed Error Taxonomy
2
+
3
+ ## Context
4
+ Generic runtime errors made retry/policy behavior inconsistent and hard to classify.
5
+
6
+ ## Decision
7
+ Define typed runtime/transport errors and centralized response-to-error mapping.
8
+
9
+ ## Alternatives rejected
10
+ - status-code checks at call sites
11
+ - adapter-specific error hierarchies
12
+
13
+ ## Consequences
14
+ - consistent retry-safe classification
15
+ - clearer policy layering
16
+ - improved observability and diagnostics
@@ -0,0 +1,16 @@
1
+ # ADR-006: Stream Runtime Ownership
2
+
3
+ ## Context
4
+ Streaming semantics were at risk of diverging across integrations and experimental repos.
5
+
6
+ ## Decision
7
+ `ollama-client` owns canonical stream contracts (lifecycle, cancellation, buffering, ordering).
8
+
9
+ ## Alternatives rejected
10
+ - per-integration stream semantics
11
+ - transport-specific user-facing stream APIs
12
+
13
+ ## Consequences
14
+ - ecosystem-wide stream consistency
15
+ - cleaner adapter and integration layering
16
+ - reduced drift risk as streaming features expand
@@ -0,0 +1,19 @@
1
+ # Ecosystem Boundaries
2
+
3
+ ## Belongs in `ollama-client`
4
+
5
+ - transport
6
+ - streaming contracts/runtime
7
+ - retries/policy orchestration
8
+ - schema/runtime contracts
9
+ - model lifecycle
10
+ - observability hooks
11
+
12
+ ## Must stay out of `ollama-client`
13
+
14
+ - vector DB integrations
15
+ - RAG pipelines
16
+ - memory systems
17
+ - workflow engines
18
+ - chain builders
19
+ - prompt template frameworks
@@ -0,0 +1,14 @@
1
+ # Dependency Graph
2
+
3
+ Allowed direction:
4
+
5
+ `ollama-client` <- extensions
6
+
7
+ Examples:
8
+ - `ollama-openai` -> `ollama-client`
9
+ - `ollama-agent` -> `ollama-client`
10
+ - `ollama-rails` -> `ollama-client`
11
+
12
+ Disallowed:
13
+ - `ollama-client` -> any extension
14
+ - circular extension dependencies
@@ -0,0 +1,9 @@
1
+ # Design Principles
2
+
3
+ 1. Determinism first
4
+ 2. Explicit APIs over magic
5
+ 3. Runtime correctness over convenience
6
+ 4. Transport-agnostic internals
7
+ 5. Streaming-native architecture
8
+ 6. Optional interoperability layers
9
+ 7. Keep core minimal and infra-grade
@@ -0,0 +1,31 @@
1
+ # Existing Repositories and Contract Ownership
2
+
3
+ This ecosystem uses existing repos as **experimental proving grounds**, not canonical runtime authorities.
4
+
5
+ ## Role Mapping
6
+
7
+ | Repository | Primary Role | Reusable Concepts | Must NOT Own |
8
+ |---|---|---|---|
9
+ | `ollama-client` | Canonical runtime kernel | transport contracts, stream semantics, error taxonomy, schema contracts, observability hooks | n/a |
10
+ | `ollama_agent` | Orchestration experimentation | planner patterns, tool execution loops, memory interface ideas | transport contracts, stream/runtime semantics, error taxonomy |
11
+ | `agent-runtime` | Higher-level execution runtime | lifecycle orchestration, scheduling concepts, workflow state transitions | inference transport, canonical response/error contracts |
12
+
13
+ ## Dependency Direction
14
+
15
+ ```text
16
+ ollama-client
17
+ ↑
18
+ ollama-openai
19
+ ↑
20
+ ollama-agent
21
+ ↑
22
+ agent-runtime
23
+ ```
24
+
25
+ Disallowed:
26
+ - `ollama-client` depending on orchestration/runtime repos.
27
+ - Upstream repos owning low-level inference contracts.
28
+
29
+ ## Rule
30
+
31
+ Experiment in higher-level repos, then stabilize and standardize contracts in `ollama-client`.
@@ -0,0 +1,24 @@
1
+ # Experimental Labs Policy
2
+
3
+ ## Principle
4
+
5
+ Experiment there, stabilize here.
6
+
7
+ ## Roles
8
+
9
+ | Repository | Role |
10
+ |---|---|
11
+ | `ollama-client` | Stable runtime kernel and canonical contract owner |
12
+ | `ollama_agent` | Orchestration experimentation |
13
+ | `agent-runtime` | Execution/runtime experimentation |
14
+
15
+ ## Not owned by experimental repos
16
+
17
+ - transport contracts
18
+ - stream semantics
19
+ - retry semantics
20
+ - response models
21
+ - schema runtime semantics
22
+ - observability semantics
23
+
24
+ These are canonicalized in `ollama-client`.
@@ -0,0 +1,28 @@
1
+ # Ecosystem Overview
2
+
3
+ ## Gems
4
+
5
+ - `ollama-client` — runtime kernel
6
+ - `ollama-openai` — OpenAI compatibility translation
7
+ - `ollama-stream` — advanced streaming runtime
8
+ - `ollama-observability` — telemetry and diagnostics
9
+ - `ollama-schema` — typed schema/repair runtime
10
+ - `ollama-testing` — mock/replay/failure simulation
11
+ - `ollama-rails` — Railtie + realtime Rails helpers
12
+ - `ollama-agent` — optional orchestration primitives
13
+ - `ollama-router` — routing and fallback logic
14
+
15
+ ## Principles
16
+
17
+ - Small deterministic core
18
+ - Optional integrations
19
+ - No abstraction soup
20
+
21
+
22
+ ## See also
23
+
24
+ - `docs/ecosystem/EXISTING_REPOS.md`
25
+ - `docs/runtime/TRANSPORT_CONTRACT.md`
26
+ - `docs/runtime/STREAM_CONTRACT.md`
27
+ - `docs/runtime/ERROR_CONTRACT.md`
28
+ - `docs/rfcs/README.md`
@@ -0,0 +1,17 @@
1
+ # Release Order
2
+
3
+ ## Phase 1
4
+ - `ollama-openai`
5
+ - `ollama-testing`
6
+
7
+ ## Phase 2
8
+ - `ollama-stream`
9
+ - `ollama-observability`
10
+
11
+ ## Phase 3
12
+ - `ollama-schema`
13
+ - `ollama-rails`
14
+
15
+ ## Phase 4
16
+ - `ollama-agent`
17
+ - `ollama-router`
@@ -0,0 +1,8 @@
1
+ # Observability Docs
2
+
3
+ Observability contracts are runtime-owned by `ollama-client`.
4
+
5
+ Future docs here should cover:
6
+ - tracing/span model
7
+ - metrics model
8
+ - diagnostic hooks
@@ -0,0 +1,7 @@
1
+ # Rails Integration Docs
2
+
3
+ This folder is reserved for ecosystem docs around `ollama-rails`:
4
+
5
+ - Railtie behavior
6
+ - ActiveJob helpers
7
+ - ActionCable/Turbo streaming integrations
@@ -0,0 +1,6 @@
1
+ # RFC-0001: Stream Runtime
2
+
3
+ Status: Draft
4
+
5
+ Define first-class `Ollama::Stream` runtime API, lifecycle, cancellation semantics,
6
+ buffering/backpressure guarantees, and parser integration boundaries.
@@ -0,0 +1,6 @@
1
+ # RFC-0002: Schema System
2
+
3
+ Status: Draft
4
+
5
+ Define typed schema runtime, coercion rules, validation contracts, and repair strategies
6
+ for deterministic structured output generation.
@@ -0,0 +1,6 @@
1
+ # RFC-0003: Observability Hooks
2
+
3
+ Status: Draft
4
+
5
+ Define canonical request/response/retry/stream lifecycle hooks and integration points
6
+ for tracing, metrics, and structured diagnostics.
@@ -0,0 +1,6 @@
1
+ # RFC-0004: Async Runtime
2
+
3
+ Status: Draft
4
+
5
+ Define async transport adapter behavior and fiber-aware runtime expectations without
6
+ changing public inference APIs.
@@ -0,0 +1,16 @@
1
+ # RFC Process
2
+
3
+ Use RFCs for ecosystem-impacting runtime proposals before implementation.
4
+
5
+ ## Initial candidates
6
+
7
+ - `0001-stream-runtime.md`
8
+ - `0002-schema-system.md`
9
+ - `0003-observability-hooks.md`
10
+ - `0004-async-runtime.md`
11
+
12
+ ## Guideline
13
+
14
+ - Proposals are discussed first.
15
+ - Canonical contracts land in `ollama-client` docs + ADRs.
16
+ - Implementation in other gems must follow accepted runtime contracts.
@@ -0,0 +1,14 @@
1
+ # Error Contract
2
+
3
+ `ollama-client` owns typed runtime error taxonomy and response-to-error mapping.
4
+
5
+ ## Principles
6
+
7
+ - Typed errors over generic exceptions
8
+ - Deterministic mapping from transport responses
9
+ - Retry-safe classification lives in core runtime contracts
10
+
11
+ ## Direction
12
+
13
+ Higher-level repos should consume and extend behavior via policy, not redefine
14
+ base runtime error classes for transport/inference semantics.
@@ -0,0 +1,28 @@
1
+ # Schema Contract
2
+
3
+ `ollama-client` owns canonical schema/runtime semantics for deterministic structured generation.
4
+
5
+ ## Scope
6
+
7
+ - Typed schema declarations
8
+ - Validation and coercion rules
9
+ - Repair strategy contracts
10
+ - Streaming structured output constraints
11
+
12
+ ## Required semantics
13
+
14
+ - Deterministic parse contract: valid structured result or typed failure
15
+ - Coercion behavior must be explicit and documented
16
+ - Validation errors must be typed and machine-readable
17
+ - Repair paths must be bounded and observable
18
+
19
+ ## Streaming requirements
20
+
21
+ - Incremental parse support
22
+ - Partial AST assembly guarantees
23
+ - Stream-time validation hooks
24
+ - Recovery behavior for malformed partial JSON
25
+
26
+ ## Ownership rule
27
+
28
+ Higher-level gems may compose convenience APIs, but must not redefine core schema semantics.