scout-ai 1.2.3 → 2.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.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +138 -50
  3. data/README.md +171 -290
  4. data/Rakefile +17 -1
  5. data/VERSION +1 -1
  6. data/doc/Improvements.md +325 -0
  7. data/doc/StartHere.md +110 -0
  8. data/doc/developer/Architecture.md +126 -0
  9. data/doc/developer/Backends.md +199 -0
  10. data/doc/developer/ChatLifecycle.md +183 -0
  11. data/doc/developer/DelegationInternals.md +295 -0
  12. data/doc/developer/DesignPrinciples.md +245 -0
  13. data/doc/developer/PromptProcessing.md +292 -0
  14. data/doc/developer/Provenance.md +317 -0
  15. data/doc/user/BuildingAgents.md +345 -0
  16. data/doc/user/Cookbook.md +333 -0
  17. data/doc/user/CoreConcepts.md +181 -0
  18. data/doc/user/Delegation.md +191 -0
  19. data/doc/user/GettingStarted.md +159 -0
  20. data/doc/user/ManagingContext.md +163 -0
  21. data/doc/user/MultiAgentWorkflows.md +256 -0
  22. data/doc/user/Python.md +159 -0
  23. data/doc/user/RunningInference.md +200 -0
  24. data/doc/user/ToolCalling.md +193 -0
  25. data/doc/user/WritingChats.md +197 -0
  26. data/lib/scout/llm/agent/chat.rb +61 -11
  27. data/lib/scout/llm/agent/delegate.rb +274 -65
  28. data/lib/scout/llm/agent/iterate.rb +2 -2
  29. data/lib/scout/llm/agent/save.rb +273 -0
  30. data/lib/scout/llm/agent/workflow.rb +164 -0
  31. data/lib/scout/llm/agent.rb +86 -61
  32. data/lib/scout/llm/ask.rb +62 -17
  33. data/lib/scout/llm/backends/anthropic.rb +9 -2
  34. data/lib/scout/llm/backends/bedrock.rb +15 -3
  35. data/lib/scout/llm/backends/default.rb +183 -99
  36. data/lib/scout/llm/backends/glm.rb +58 -0
  37. data/lib/scout/llm/backends/huggingface.rb +196 -26
  38. data/lib/scout/llm/backends/ollama.rb +13 -1
  39. data/lib/scout/llm/backends/openai.rb +0 -2
  40. data/lib/scout/llm/backends/openwebui.rb +20 -13
  41. data/lib/scout/llm/backends/relay.rb +22 -22
  42. data/lib/scout/llm/backends/responses.rb +1 -1
  43. data/lib/scout/llm/chat/agent_meta.rb +264 -0
  44. data/lib/scout/llm/chat/annotation.rb +39 -10
  45. data/lib/scout/llm/chat/parse.rb +28 -6
  46. data/lib/scout/llm/chat/persist.rb +25 -0
  47. data/lib/scout/llm/chat/process/clear.rb +41 -6
  48. data/lib/scout/llm/chat/process/files.rb +21 -6
  49. data/lib/scout/llm/chat/process/meta.rb +421 -34
  50. data/lib/scout/llm/chat/process/options.rb +21 -1
  51. data/lib/scout/llm/chat/process/tools.rb +56 -15
  52. data/lib/scout/llm/chat/process.rb +4 -0
  53. data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
  54. data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
  55. data/lib/scout/llm/chat/prompt.rb +48 -0
  56. data/lib/scout/llm/chat/provenance.rb +775 -0
  57. data/lib/scout/llm/chat/tool_calls.rb +76 -0
  58. data/lib/scout/llm/chat.rb +18 -2
  59. data/lib/scout/llm/embed.rb +11 -3
  60. data/lib/scout/llm/image.rb +86 -0
  61. data/lib/scout/llm/mcp.rb +10 -2
  62. data/lib/scout/llm/rag.rb +3 -3
  63. data/lib/scout/llm/tools/call.rb +160 -11
  64. data/lib/scout/llm/tools/knowledge_base.rb +1 -1
  65. data/lib/scout/llm/tools/workflow.rb +32 -16
  66. data/lib/scout/model/python/huggingface/causal.rb +23 -5
  67. data/lib/scout/model/python/huggingface.rb +2 -1
  68. data/lib/scout-ai.rb +1 -0
  69. data/python/README.md +197 -14
  70. data/python/scout_ai/huggingface/eval.py +245 -34
  71. data/python/tests/test_huggingface_eval.py +58 -0
  72. data/research/ChatAnalyst-required-changes.md +167 -0
  73. data/research/agent-delegation-analysis.md +810 -0
  74. data/research/agent-meta-provenance-integration-plan.md +622 -0
  75. data/research/agent-workflow-analysis.md +1120 -0
  76. data/research/backends-analysis.md +836 -0
  77. data/research/chat-core-analysis.md +946 -0
  78. data/research/chatanalyst-provenance/00-baseline.md +30 -0
  79. data/research/chatanalyst-provenance/01-repo-map.md +60 -0
  80. data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
  81. data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
  82. data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
  83. data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
  84. data/research/chatanalyst-provenance/07-critic-review.md +25 -0
  85. data/research/chatanalyst-provenance/final-report.md +45 -0
  86. data/research/chatanalyst-provenance/resumption.md +37 -0
  87. data/research/coding-philosophy-analysis.md +928 -0
  88. data/research/commands-analysis.md +947 -0
  89. data/research/multi-agent-patterns-analysis.md +853 -0
  90. data/research/prompt-strategies-analysis.md +630 -0
  91. data/research/prov-verbosity-fix-notes.md +77 -0
  92. data/research/provenance-analysis.md +469 -0
  93. data/research/provenance-navigation-design.md +640 -0
  94. data/research/synthesis-report.md +487 -0
  95. data/research/tools-system-analysis.md +779 -0
  96. data/scout-ai.gemspec +100 -11
  97. data/scout_commands/agent/ask +13 -3
  98. data/scout_commands/agent/kb +2 -0
  99. data/scout_commands/llm/ask +11 -4
  100. data/scout_commands/llm/md +76 -0
  101. data/scout_commands/llm/process_queries +48 -0
  102. data/scout_commands/llm/prov +602 -0
  103. data/scout_commands/llm/word +71 -0
  104. data/scout_commands/workflow/mcp +43 -0
  105. data/share/word/reference.docx +0 -0
  106. data/test/etc/AI/mock.yaml +11 -0
  107. data/test/fixtures/backends/anthropic.json +19 -0
  108. data/test/fixtures/backends/anthropic_tool_use.json +24 -0
  109. data/test/fixtures/backends/bedrock.json +8 -0
  110. data/test/fixtures/backends/bedrock_embedding.json +3 -0
  111. data/test/fixtures/backends/bedrock_tool_use.json +17 -0
  112. data/test/fixtures/backends/ollama.json +16 -0
  113. data/test/fixtures/backends/ollama_tool_call.json +27 -0
  114. data/test/fixtures/backends/openai_chat.json +21 -0
  115. data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
  116. data/test/fixtures/backends/responses.json +33 -0
  117. data/test/fixtures/backends/responses_tool_call.json +28 -0
  118. data/test/integration/README.md +32 -0
  119. data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
  120. data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
  121. data/test/integration/scout/llm/backends/test_relay.rb +52 -0
  122. data/test/integration/scout/llm/test_infrastructure.rb +74 -0
  123. data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
  124. data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
  125. data/test/integration/scout/model/test_base.rb +91 -0
  126. data/test/scout/llm/agent/test_chat.rb +8 -2
  127. data/test/scout/llm/agent/test_save.rb +413 -0
  128. data/test/scout/llm/agent/test_workflow.rb +110 -0
  129. data/test/scout/llm/backends/test_anthropic.rb +93 -10
  130. data/test/scout/llm/backends/test_bedrock.rb +118 -2
  131. data/test/scout/llm/backends/test_huggingface.rb +137 -42
  132. data/test/scout/llm/backends/test_ollama.rb +70 -20
  133. data/test/scout/llm/backends/test_openwebui.rb +42 -40
  134. data/test/scout/llm/backends/test_relay.rb +4 -2
  135. data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
  136. data/test/scout/llm/chat/process/test_meta.rb +518 -0
  137. data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
  138. data/test/scout/llm/chat/test_agent_meta.rb +357 -0
  139. data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
  140. data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
  141. data/test/scout/llm/chat/test_parse.rb +70 -15
  142. data/test/scout/llm/chat/test_prov_cli.rb +274 -0
  143. data/test/scout/llm/chat/test_provenance.rb +240 -0
  144. data/test/scout/llm/chat/test_tool_calls.rb +38 -0
  145. data/test/scout/llm/test_agent.rb +13 -36
  146. data/test/scout/llm/test_ask.rb +75 -52
  147. data/test/scout/llm/test_chat.rb +107 -13
  148. data/test/scout/llm/test_embed.rb +48 -0
  149. data/test/scout/llm/test_rag.rb +23 -16
  150. data/test/scout/llm/test_tools.rb +12 -1
  151. data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
  152. data/test/scout/llm/tools/test_mcp.rb +5 -3
  153. data/test/scout/llm/tools/test_workflow.rb +23 -2
  154. data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
  155. data/test/scout/model/python/huggingface/test_causal.rb +9 -3
  156. data/test/scout/model/python/huggingface/test_classification.rb +11 -2
  157. data/test/scout/model/python/test_torch.rb +2 -0
  158. data/test/scout/model/python/torch/test_helpers.rb +4 -0
  159. data/test/scout/model/test_base.rb +4 -2
  160. data/test/support/availability.rb +231 -0
  161. data/test/support/fake_clients.rb +138 -0
  162. data/test/support/fixtures.rb +21 -0
  163. data/test/support/infrastructure_probes.rb +136 -0
  164. data/test/support/mock_backend.rb +215 -0
  165. data/test/test_helper.rb +32 -2
  166. metadata +99 -10
  167. data/doc/Agent.md +0 -327
  168. data/doc/Chat.md +0 -458
  169. data/doc/LLM.md +0 -340
  170. data/doc/RAG.md +0 -129
  171. data/scout_commands/documenter +0 -148
  172. data/test/scout/llm/backends/test_openai.rb +0 -192
  173. data/test/scout/llm/backends/test_responses.rb +0 -238
  174. data/test/scout/llm/test_parse.rb +0 -98
@@ -0,0 +1,928 @@
1
+ > **Disclaimer:** This is an architectural investigation, not normative
2
+ > documentation. It was produced during a documentation-revamp effort and may
3
+ > be outdated relative to the current codebase. Treat it as supporting
4
+ > reference material. For maintained documentation, see
5
+ > [../../doc/](../../doc/).
6
+ >
7
+
8
+
9
+ # Scout-AI Coding Philosophy & Idioms
10
+
11
+ > **Purpose:** Enable coding agents (and humans) to write code that fits the
12
+ > existing Scout-AI style. This document is a field guide to the abstractions,
13
+ > design principles, Ruby idioms, naming conventions, and anti-patterns that
14
+ > make the codebase elegant and expressive.
15
+
16
+ ---
17
+
18
+ ## 1. Core Abstractions
19
+
20
+ Scout-AI is built from a small number of composable abstractions. Each one
21
+ plays a single, well-defined role. Understanding how they compose is the key
22
+ to extending the library.
23
+
24
+ ### 1.1 The six pillars
25
+
26
+ | Abstraction | Module / Class | File | Role |
27
+ |-------------------------|-----------------------|----------------------------------|-----------------------------------------------------------------------------------------------|
28
+ | **Chat** | `Chat` (Annotation) | `lib/scout/llm/chat.rb` + `chat/`| A conversation: a plain `Array` of message `Hash`es, annotated with rich DSL methods. |
29
+ | **Agent** | `LLM::Agent` | `lib/scout/llm/agent.rb` + sub-files | Stateful wrapper around a Chat with a start_chat, a workflow, knowledge bases, tool wiring, and delegation. |
30
+ | **AgentWorkflow** | `AgentWorkflow` mixin | `lib/scout/llm/agent/workflow.rb`| A `Workflow` mixin that adds `chat_task`, `helper :agent`, and `helper :log_agent` for multi-agent strategies encoded as Scout workflows. |
31
+ | **Backend** | `LLM::Backend` + per-backend modules | `lib/scout/llm/backends/` | Adapter to a specific LLM provider (OpenAI, Anthropic, Ollama, etc.). Shares logic via `Backend::ClassMethods` and overrides via `prepend`. |
32
+ | **Tools** | `LLM` module methods | `lib/scout/llm/tools/` | Definition and execution of callable tools: workflow tasks, knowledge-base queries, MCP servers, code execution. |
33
+ | **Annotation** | `Annotation` (from scout-essentials) | `lib/scout/annotation.rb` | Non-invasive metadata injection onto existing objects (Arrays, Hashes, etc.) without subclassing or wrapping. Chat uses this to add DSL methods to a plain Array. |
34
+
35
+ ### 1.2 How they compose
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────┐
39
+ │ LLM (module) │
40
+ │ LLM.ask ← entry point for all inference │
41
+ │ LLM.chat ← parse/compile chat files │
42
+ │ LLM.load_agent ← resolve agent directories │
43
+ └───────────────┬───────────────────────────────┘
44
+ │
45
+ ┌───────────────────┼───────────────────────┐
46
+ ▼ ▼ ▼
47
+ ┌──────────┐ ┌──────────────┐ ┌──────────────┐
48
+ │ Backend │ │ LLM::Agent │ │ Tools │
49
+ │ adapter │ │ (stateful) │ │ (WF/KB/MCP) │
50
+ └──────────┘ └──────┬───────┘ └──────┬───────┘
51
+ │ holds │
52
+ ┌─────▼─────┐ ┌──────▼──────┐
53
+ │ Chat │◄────────►│ Workflow │
54
+ │ (Array + │ task │ tasks as │
55
+ │ DSL) │ tools │ tools │
56
+ └───────────┘ └─────────────┘
57
+ │ extends
58
+ ┌─────▼─────┐
59
+ │ Annotation│ (non-invasive mixin)
60
+ └───────────┘
61
+ ```
62
+
63
+ In words:
64
+
65
+ 1. **`LLM.ask`** is the universal entry point. It accepts any string, file, or
66
+ Array of Hashes, compiles it via `Chat`, resolves options, selects a
67
+ `Backend`, and returns the response as a `Chat` (annotated Array).
68
+ 2. **`LLM::Agent`** wraps a `Chat` with persistent state (start_chat,
69
+ current_chat), a `Workflow` (for workflow-backed `ask`), knowledge bases,
70
+ and delegation methods (`socialize`, `delegate`, `ask_agent`).
71
+ 3. **`AgentWorkflow`** is a `Workflow` mixin that provides the `chat_task` DSL
72
+ and agent lifecycle helpers. Multi-agent strategies are encoded as Scout
73
+ workflows that `include_workflow AgentWorkflow`.
74
+ 4. **`Backend`** modules translate the Chat format into provider-specific API
75
+ calls and translate responses back. The composition pattern is
76
+ `class << self; prepend XMethods; include Backend::ClassMethods; end`.
77
+ 5. **Tools** are defined as tool-definition Hashes paired with execution
78
+ blocks. They are merged into the `options[:tools]` IndiferentHash and
79
+ dispatched by the backend.
80
+ 6. **Annotation** powers the Chat DSL: a plain Array is annotated
81
+ (`Chat.setup(array)`) so it gains `.user`, `.system`, `.follow`, `.ask`,
82
+ `.chat`, etc., without being a subclass or a wrapper object.
83
+
84
+ ### 1.3 Dependency graph
85
+
86
+ ```
87
+ scout-ai.rb
88
+ └─ scout/llm/ask.rb (requires scout, chat)
89
+ └─ scout/llm/chat.rb (requires chat/annotation, chat/parse, chat/process, chat/prompt, chat/persist, tools, utils)
90
+ └─ scout/llm/agent.rb (requires ask, agent/chat, agent/iterate, agent/delegate, agent/workflow)
91
+ └─ scout/llm/embed.rb
92
+ └─ scout/llm/image.rb
93
+ └─ scout/llm/tools/ (workflow, knowledge_base, mcp, call)
94
+ └─ scout/llm/backends/ (default + 10 provider adapters)
95
+ ```
96
+
97
+ Key dependency direction: **Agent → Chat → Annotation**.
98
+ Backends depend on Chat and Backend::ClassMethods, not on Agent.
99
+ AgentWorkflow depends on Agent and Chat, not on specific Backends.
100
+
101
+ ---
102
+
103
+ ## 2. Design Philosophy
104
+
105
+ ### 2.1 Abstraction-first
106
+
107
+ Every concept in Scout-AI is an abstraction with a crisp boundary:
108
+
109
+ - A **Chat** is "a conversation" — nothing more, nothing less.
110
+ - An **Agent** is "a stateful conversation holder with tools."
111
+ - A **Backend** is "an adapter to a model API."
112
+
113
+ The code rarely mixes concerns. For example, `LLM.ask` never contains
114
+ provider-specific logic; it dispatches to `Backend::OpenAI.ask` or
115
+ `Backend::Anthropic.ask`. The Backend modules never hold state; they are
116
+ stateless module-method adapters.
117
+
118
+ **Why this matters:** New features should be expressed as a new abstraction or
119
+ an extension of an existing one, not as inline logic scattered across files.
120
+
121
+ ### 2.2 Module composition over inheritance
122
+
123
+ Scout-AI avoids deep class hierarchies. Instead, it composes behavior through
124
+ Ruby modules:
125
+
126
+ ```ruby
127
+ # Agent's behavior is split across multiple files that reopen the class:
128
+ # lib/scout/llm/agent.rb — core
129
+ # lib/scout/llm/agent/chat.rb — chat delegation methods
130
+ # lib/scout/llm/agent/iterate.rb — iteration patterns
131
+ # lib/scout/llm/agent/delegate.rb — multi-agent delegation
132
+ # lib/scout/llm/agent/workflow.rb — AgentWorkflow mixin + chat_task DSL
133
+ ```
134
+
135
+ The `Chat` module is `extend Annotation` — it is a **module**, not a class.
136
+ The annotated object is whatever you pass in (typically a plain Array). This
137
+ is the "annotate, don't wrap" philosophy.
138
+
139
+ ### 2.3 The Chat-as-data philosophy
140
+
141
+ This is the single most important design decision in Scout-AI:
142
+
143
+ > **A Chat is a plain `Array` of message `Hash`es, not an opaque object.**
144
+
145
+ The `Chat` module annotates an Array to add DSL methods, but the underlying
146
+ data structure is always accessible:
147
+
148
+ ```ruby
149
+ chat = Chat.setup([])
150
+ chat.user("Hello")
151
+ chat.system("You are helpful")
152
+
153
+ # chat IS an Array:
154
+ chat.class # => Array
155
+ chat.length # => 2
156
+ chat.first[:role] # => "user"
157
+ chat.first[:content] # => "Hello"
158
+ chat.select { |m| m[:role] == 'system' } # works
159
+ ```
160
+
161
+ This means:
162
+
163
+ - Chats are **serializable** to plain text (the `.chat` file format) and back.
164
+ - Chats are **composable**: `intro + coda` just concatenates Arrays.
165
+ - Chats are **introspectable**: you can filter, map, select directly.
166
+ - Chats are **cacheable**: `Persist.persist` hashes the message Array.
167
+ - No lock-in: you can drop down to Array operations at any time.
168
+
169
+ ### 2.4 The annotation/metadata pattern
170
+
171
+ Scout-AI uses scout-essentials' `Annotation` system to add methods to existing
172
+ objects **non-invasively**:
173
+
174
+ ```ruby
175
+ module Chat
176
+ extend Annotation # Chat is now an annotation module
177
+
178
+ def user(content)
179
+ message(:user, content)
180
+ end
181
+ # ... 40+ DSL methods
182
+ end
183
+
184
+ # Usage: annotate a plain Array
185
+ messages = [{ role: 'user', content: 'Hi' }]
186
+ Chat.setup(messages) # messages is still an Array, now with Chat methods
187
+ messages.system("Be brief") # appends { role: 'system', content: 'Be brief' }
188
+ messages.ask # calls LLM.ask with the messages
189
+ ```
190
+
191
+ The annotation:
192
+
193
+ - Adds methods to the **singleton class** of the specific object instance.
194
+ - Does **not** change the object's class (it remains `Array`).
195
+ - Is **removable**: `Annotation.purge(obj)` strips annotations.
196
+ - Carries **typed metadata**: `annotation_types` tracks which annotations are applied.
197
+
198
+ This pattern is used for:
199
+
200
+ | Annotation | Annotates | Purpose |
201
+ |------------|-----------------|---------------------------------------------------|
202
+ | `Chat` | `Array` | Conversation DSL (user, system, ask, follow, etc.)|
203
+ | `Step` | `String` (path) | Workflow job metadata (dependencies, info, etc.) |
204
+
205
+ ### 2.5 Convention over configuration
206
+
207
+ Scout-AI discovers components by convention rather than registration:
208
+
209
+ **Agent resolution** (`LLM.load_agent`):
210
+ 1. If the name is a file path → `load` it.
211
+ 2. If it's a directory with `agent.rb` → `load` that file.
212
+ 3. Otherwise check (in order):
213
+ - `Scout.workflows[<name>]` (workflow directory)
214
+ - `Scout.Agent[<name>]` (agent var directory)
215
+ - `Scout.var.Agent[<name>]` (fallback)
216
+ - `Scout.chats.Agent[<name>]` (chat agent directory)
217
+ - `Scout.chats[<name>]` (general chat directory)
218
+
219
+ **Agent directory structure** (convention):
220
+ ```
221
+ <agent_name>/
222
+ workflow.rb ← Scout Workflow with an :ask task
223
+ start_chat ← initial chat messages (text, chat-file format)
224
+ knowledge_base/ ← optional KB directory
225
+ python/ ← optional Python tasks
226
+ ```
227
+
228
+ **Backend convention:**
229
+ - Each backend is a module under `LLM::` (e.g., `LLM::OpenAI`, `LLM::Anthropic`).
230
+ - It composes via `class << self; prepend XMethods; include Backend::ClassMethods; end`.
231
+ - It exposes `TAG` and `DEFAULT_MODEL` constants.
232
+
233
+ **Endpoint convention:**
234
+ - Named endpoints are YAML files in `~/.scout/etc/AI/<name>`.
235
+ - Selected by `endpoint: :name` in options or `--endpoint name` on CLI.
236
+
237
+ ### 2.6 DSL patterns
238
+
239
+ Several DSLs exist in the codebase:
240
+
241
+ #### Chat DSL (instance methods on annotated Arrays)
242
+ ```ruby
243
+ chat.user("...") # append user message
244
+ chat.system("...") # append system message
245
+ chat.assistant("...") # append assistant message
246
+ chat.file("README.md") # append file reference
247
+ chat.task(WF, :task, opt: val) # append workflow task reference
248
+ chat.follow(other_chat) # append another chat's messages
249
+ chat.option(:model, "gpt-5") # append sticky option
250
+ chat.ask(options) # call LLM.ask and return response
251
+ chat.chat(options) # call LLM.ask, append response, return answer
252
+ chat.json(only_ask: true) # request JSON output
253
+ ```
254
+
255
+ #### Agent DSL (via method_missing to current_chat)
256
+ ```ruby
257
+ agent.user("...") # delegates to current_chat.user
258
+ agent.system("...") # delegates to current_chat.system
259
+ agent.chat # calls ask and appends response
260
+ agent.ask # calls LLM.ask with current_chat
261
+ agent.follow(chat) # appends to current_chat
262
+ agent.branch # creates a new chat from current_chat
263
+ ```
264
+
265
+ #### Workflow DSL (from scout-gear, extended by AgentWorkflow)
266
+ ```ruby
267
+ module MyStrategy
268
+ extend Workflow
269
+ self.include_workflow AgentWorkflow
270
+
271
+ input :chat, :text, 'Chat input'
272
+
273
+ chat_task :work do
274
+ agent = self.agent(nil, chat: chat)
275
+ agent.user("Do the work")
276
+ agent.chat
277
+ end
278
+ end
279
+ ```
280
+
281
+ #### Backend composition DSL
282
+ ```ruby
283
+ module LLM
284
+ module MyBackendMethods
285
+ def query(client, messages, tools = [], parameters = {})
286
+ # override
287
+ end
288
+ end
289
+
290
+ module MyBackend
291
+ TAG = 'mybackend'
292
+ DEFAULT_MODEL = 'my-model-v1'
293
+
294
+ class << self
295
+ prepend MyBackendMethods # overrides
296
+ include Backend::ClassMethods # shared logic
297
+ end
298
+ end
299
+ end
300
+ ```
301
+
302
+ ---
303
+
304
+ ## 3. Key Ruby Idioms Used
305
+
306
+ ### 3.1 `extend` vs `include` vs `prepend`
307
+
308
+ | Idiom | Used for | Example |
309
+ |-------------|-----------------------------------------------|-------------------------------------------------------------------------|
310
+ | `extend` | Adding class/singleton methods to a module | `module Chat; extend Annotation; end` — Chat gets `.setup`, `.purge` |
311
+ | `include` | Adding instance methods to a class | `include Backend::ClassMethods` — shared backend logic |
312
+ | `prepend` | Overriding methods while calling `super` | `prepend OpenAIMethods` — overrides `query` while `ask` stays in base |
313
+
314
+ **Backend composition pattern** (the most important `prepend`/`include` usage):
315
+
316
+ ```ruby
317
+ # The shared implementation lives in Backend::ClassMethods (include)
318
+ # The overrides live in a *Methods module (prepend)
319
+ # The dispatch order is: *Methods (prepend) → Backend::ClassMethods (include)
320
+
321
+ class << self
322
+ prepend OpenAIMethods # called FIRST — can override query, format_tool_definitions
323
+ include Backend::ClassMethods # called SECOND — provides ask, embed, process_tool_calls
324
+ end
325
+ ```
326
+
327
+ This allows `Backend::ClassMethods#ask` to call `query` and have Ruby dispatch
328
+ to `OpenAIMethods#query` (the prepend).
329
+
330
+ **Agent's `extend Workflow`:**
331
+ ```ruby
332
+ @workflow ||= begin
333
+ m = Module.new
334
+ m.extend Workflow # The module gains task, input, helper, etc.
335
+ m.name ||= 'ScoutAgent'
336
+ m.tasks = {}
337
+ m
338
+ end
339
+ ```
340
+
341
+ ### 3.2 `IndiferentHash` usage
342
+
343
+ `IndiferentHash` (from scout-essentials) is used everywhere options are
344
+ handled. It provides symbol/string-indifferent access plus utility methods:
345
+
346
+ ```ruby
347
+ # Setup any hash as indifferent
348
+ options = IndiferentHash.setup({})
349
+
350
+ # Add defaults without overwriting
351
+ options = IndiferentHash.add_defaults(options, model: 'gpt-5')
352
+
353
+ # Extract and remove keys in one call
354
+ backend, persist = IndiferentHash.process_options(options, :backend, :persist, persist: true)
355
+
356
+ # Parse option strings
357
+ options = IndiferentHash.parse_options("model=gpt-5 backend=responses")
358
+ ```
359
+
360
+ **Convention:** Always `IndiferentHash.setup` any hash that comes from user
361
+ input, parsed JSON, or kwargs. This prevents `:model` vs `'model'` bugs.
362
+
363
+ ### 3.3 `Path` / `Open` / `TSV` from scout-essentials
364
+
365
+ - **`Path`**: Smart path objects with `.find`, `.exists?`, globbing, and
366
+ Scout's path system (`Scout.var`, `Scout.chats`, `Scout.workflows`).
367
+ ```ruby
368
+ path = Scout.chats.Agent['Worker'].start_chat
369
+ path.exists? # => true/false
370
+ path.find # => resolved absolute path
371
+ ```
372
+
373
+ - **`Open`**: Filesystem utilities that work with Path and String:
374
+ ```ruby
375
+ Open.exists?(path)
376
+ Open.write(path, content)
377
+ Open.read(path)
378
+ Open.remote?(url) # checks if it's a URL
379
+ ```
380
+
381
+ - **`TSV`**: Tab-separated value manipulation with `TSV.traverse` for
382
+ parallel iteration:
383
+ ```ruby
384
+ TSV.traverse(dict, **kwargs, &block)
385
+ ```
386
+
387
+ ### 3.4 Module as namespace + mixin
388
+
389
+ Modules serve double duty as both namespaces and mixin providers:
390
+
391
+ ```ruby
392
+ module LLM # Namespace: LLM.ask, LLM.chat, LLM.load_agent
393
+ module Backend # Namespace: Backend::ClassMethods, Backend::BackendException
394
+ module ClassMethods # Mixin: included into backend singletons
395
+ def ask(messages, options) # shared implementation
396
+ ...
397
+ end
398
+ end
399
+ end
400
+
401
+ module OpenAI # Namespace: the backend itself
402
+ # ...also a mixin target via singleton class composition
403
+ end
404
+ end
405
+ ```
406
+
407
+ ### 3.5 Block-based DSLs
408
+
409
+ Block-based DSLs are used for task definitions and tool execution:
410
+
411
+ ```ruby
412
+ # Workflow task with block
413
+ task :ask => :text do |chat|
414
+ # self is the workflow instance
415
+ # instance_exec gives access to helpers
416
+ end
417
+
418
+ # Tool execution block
419
+ LLM.ask(messages, tools: tools) do |task_name, parameters|
420
+ workflow.job(task_name, parameters).run
421
+ end
422
+
423
+ # Persist with block (only executes if cache miss)
424
+ Persist.persist(endpoint, :json, ...) do
425
+ # expensive computation
426
+ end
427
+ ```
428
+
429
+ ### 3.6 `method_missing` delegation
430
+
431
+ The `Agent` class delegates unknown methods to `current_chat`:
432
+
433
+ ```ruby
434
+ class Agent
435
+ def method_missing(name, ...)
436
+ current_chat.send(name, ...)
437
+ end
438
+ end
439
+ ```
440
+
441
+ This means `agent.user(...)`, `agent.system(...)`, `agent.file(...)` all
442
+ transparently delegate to the Chat DSL without defining each method.
443
+
444
+ ### 3.7 Configuration cascade
445
+
446
+ Configuration is resolved through a priority chain via `Scout::Config.get`:
447
+
448
+ ```ruby
449
+ Scout::Config.get(:model, :ask, :llm, env: 'ASK_MODEL,LLM_MODEL', default: 'gpt-5-nano')
450
+ ```
451
+
452
+ Priority order (highest first):
453
+ 1. Explicit option passed in code/options hash
454
+ 2. Environment variable (from `env:` list)
455
+ 3. Config file entries (matching tokens)
456
+ 4. Default value
457
+
458
+ ---
459
+
460
+ ## 4. Naming Conventions
461
+
462
+ ### 4.1 File naming
463
+
464
+ | Pattern | Convention | Example |
465
+ |----------------------------------|-----------------------------------------|----------------------------------|
466
+ | Top-level modules | `<module>.rb` | `lib/scout/llm/ask.rb` |
467
+ | Sub-modules (namespace + body) | `<namespace>/<name>.rb` | `lib/scout/llm/agent/delegate.rb`|
468
+ | Annotation modules | `<name>/annotation.rb` | `lib/scout/llm/chat/annotation.rb`|
469
+ | Processing modules | `<name>/process/<aspect>.rb` | `lib/scout/llm/chat/process/tools.rb`|
470
+ | Backend modules | `backends/<provider>.rb` | `lib/scout/llm/backends/openai.rb`|
471
+ | Agent directory files | lowercase, no extension | `start_chat`, `workflow.rb` |
472
+
473
+ **Key rule:** File paths mirror module nesting.
474
+ `LLM::Agent` → `lib/scout/llm/agent.rb`.
475
+ `LLM::Agent` behavior extensions → `lib/scout/llm/agent/chat.rb`.
476
+
477
+ ### 4.2 Method naming
478
+
479
+ | Category | Convention | Examples |
480
+ |------------------|-----------------------------------|-----------------------------------------|
481
+ | DSL actions | verb (lowercase) | `user`, `system`, `ask`, `follow` |
482
+ | Queries | noun or predicate | `current_chat`, `answer`, `final` |
483
+ | Class methods | `self.` prefix on module | `LLM.ask`, `Chat.parse`, `Chat.setup` |
484
+ | Helpers (WF) | `helper :name do ... end` | `helper :agent`, `helper :chat` |
485
+ | Predicates | end with `?` | `exists?`, `remote?`, `is_filename?` |
486
+ | Destructive | end with `!` (rare) | — |
487
+ | Convention: `setup` | class method that annotates | `Chat.setup(array)`, `IndiferentHash.setup(hash)` |
488
+
489
+ ### 4.3 Variable conventions
490
+
491
+ | Variable | Convention | Example |
492
+ |-----------------|-----------------------------------------|-------------------------------------------|
493
+ | Messages/chats | `messages`, `chat`, `coda`, `intro` | `messages = LLM.chat(question)` |
494
+ | Options | `options` (always IndiferentHash) | `options = IndiferentHash.setup({})` |
495
+ | Path objects | `path`, `dir`, `file` (Path-typed) | `path = Scout.chats.find['hello']` |
496
+ | Agent instances | `agent` | `agent = LLM.load_agent('Worker')` |
497
+ | Blocks/lambdas | `block`, named with `&` | `&block` |
498
+
499
+ ---
500
+
501
+ ## 5. How to Write Idiomatic Scout-AI Code
502
+
503
+ ### 5.1 Extending the Chat DSL
504
+
505
+ To add a new message role or convenience method:
506
+
507
+ ```ruby
508
+ # GOOD: Add to the Chat annotation module
509
+ module Chat
510
+ def screenshot(file)
511
+ message(:image, file) # or a new role
512
+ end
513
+ end
514
+ ```
515
+
516
+ This automatically becomes available on any annotated Array and via
517
+ `agent.method_missing`.
518
+
519
+ ### 5.2 Adding a new backend
520
+
521
+ ```ruby
522
+ require_relative 'default'
523
+
524
+ module LLM
525
+ module MyProviderMethods
526
+ # Override provider-specific methods
527
+ def query(client, messages, tools = [], parameters = {})
528
+ parameters[:messages] = messages
529
+ parameters[:tools] = format_tool_definitions(tools) if tools&.any?
530
+ client.chat(parameters: parameters)
531
+ end
532
+
533
+ def format_tool_definitions(tools)
534
+ # translate to provider format
535
+ end
536
+
537
+ def client(options, messages = nil)
538
+ url, key = IndiferentHash.process_options(options, :url, :key)
539
+ MyProvider::Client.new(api_key: key, base_url: url)
540
+ end
541
+ end
542
+
543
+ module MyProvider
544
+ TAG = 'myprovider'
545
+ DEFAULT_MODEL = 'my-model-v1'
546
+
547
+ class << self
548
+ prepend MyProviderMethods
549
+ include Backend::ClassMethods
550
+ end
551
+ end
552
+ end
553
+ ```
554
+
555
+ Then register it in the dispatch `case` statement in `LLM.ask`:
556
+
557
+ ```ruby
558
+ when :myprovider, "myprovider"
559
+ require_relative 'backends/myprovider'
560
+ LLM::MyProvider.ask(messages, options, &block)
561
+ ```
562
+
563
+ ### 5.3 Building a multi-agent strategy
564
+
565
+ ```ruby
566
+ require 'scout-ai'
567
+
568
+ module MyStrategy
569
+ extend Workflow
570
+ self.include_workflow AgentWorkflow
571
+
572
+ input :chat, :text, 'Chat input'
573
+ extension :chat
574
+
575
+ chat_task :ask do
576
+ # Create an agent from a chat
577
+ agent = self.agent(nil, chat: chat)
578
+
579
+ # Use the Chat DSL through method_missing
580
+ agent.user("Analyze this request and produce a plan.")
581
+
582
+ # The agent.chat method calls LLM.ask and appends the response
583
+ plan = agent.answer
584
+
585
+ # Delegate to a specialist
586
+ specialist = self.agent('Worker', chat: nil)
587
+ specialist.user("Execute: #{plan}")
588
+ specialist.chat
589
+
590
+ # Log agent conversations
591
+ log_agent(specialist, 'worker')
592
+ log_agent(agent, 'planner')
593
+
594
+ specialist.answer
595
+ end
596
+ end
597
+ ```
598
+
599
+ ### 5.4 Adding a tool
600
+
601
+ ```ruby
602
+ # Register a tool on an agent
603
+ agent.other_options[:tools][:search] = [
604
+ search_block, # Proc: (task_name, parameters) => result
605
+ {
606
+ name: 'search',
607
+ description: 'Search the web',
608
+ type: 'function',
609
+ function: {
610
+ name: 'search',
611
+ description: 'Search the web',
612
+ parameters: {
613
+ type: 'object',
614
+ properties: { query: { type: 'string' } },
615
+ required: ['query']
616
+ }
617
+ }
618
+ }
619
+ ]
620
+ ```
621
+
622
+ ### 5.5 Anti-patterns (what NOT to do)
623
+
624
+ #### ❌ Don't create wrapper classes for Chat
625
+
626
+ ```ruby
627
+ # BAD: Wrapping Chat in a custom class
628
+ class MyConversation
629
+ def initialize
630
+ @messages = []
631
+ end
632
+ def add_user(text)
633
+ @messages << { role: 'user', content: text }
634
+ end
635
+ end
636
+
637
+ # GOOD: Use the annotation pattern
638
+ chat = Chat.setup([])
639
+ chat.user("Hello")
640
+ ```
641
+
642
+ #### ❌ Don't hardcode provider logic in LLM.ask
643
+
644
+ ```ruby
645
+ # BAD
646
+ def self.ask(question, options = {})
647
+ if options[:provider] == 'openai'
648
+ # 50 lines of OpenAI-specific code inline
649
+ end
650
+ end
651
+
652
+ # GOOD: Dispatch to a backend module
653
+ def self.ask(question, options = {})
654
+ case backend
655
+ when :openai
656
+ require_relative 'backends/openai'
657
+ LLM::OpenAI.ask(messages, options, &block)
658
+ end
659
+ end
660
+ ```
661
+
662
+ #### ❌ Don't use plain Hash when options come from user input
663
+
664
+ ```ruby
665
+ # BAD: String/symbol key bugs
666
+ def ask(question, options = {})
667
+ model = options[:model] # fails if user passed 'model'
668
+ end
669
+
670
+ # GOOD: IndiferentHash
671
+ def ask(question, options = {})
672
+ options = IndiferentHash.setup(options)
673
+ model = options[:model] # works for both :model and 'model'
674
+ end
675
+ ```
676
+
677
+ #### ❌ Don't subclass to add behavior
678
+
679
+ ```ruby
680
+ # BAD: Inheritance hierarchy
681
+ class SpecialChat < Array
682
+ def user(content)
683
+ self << { role: 'user', content: content }
684
+ end
685
+ end
686
+
687
+ # GOOD: Annotation (open class, no hierarchy)
688
+ module Chat
689
+ extend Annotation
690
+ def user(content)
691
+ message(:user, content)
692
+ end
693
+ end
694
+ # Then: Chat.setup(any_array)
695
+ ```
696
+
697
+ #### ❌ Don't scatter file I/O without Path/Open
698
+
699
+ ```ruby
700
+ # BAD
701
+ File.read("/hardcoded/path/#{name}")
702
+
703
+ # GOOD
704
+ path = Scout.var.Agent[name].start_chat
705
+ content = Open.read(path.find) if path.exists?
706
+ ```
707
+
708
+ #### ❌ Don't define methods on Agent that duplicate Chat
709
+
710
+ ```ruby
711
+ # BAD: Redundant delegation
712
+ class Agent
713
+ def add_user_message(text)
714
+ current_chat << { role: 'user', content: text }
715
+ end
716
+ end
717
+
718
+ # GOOD: method_missing already delegates to current_chat
719
+ agent.user(text) # works automatically
720
+ ```
721
+
722
+ ---
723
+
724
+ ## 6. Examples: Good vs Bad Code
725
+
726
+ ### Example 1: Processing a chat before sending to the model
727
+
728
+ #### ❌ Non-idiomatic
729
+ ```ruby
730
+ class ChatProcessor
731
+ def initialize(chat_array)
732
+ @chat = chat_array
733
+ end
734
+
735
+ def remove_tool_messages
736
+ @chat.reject! { |m| m[:role] == 'tool' }
737
+ end
738
+
739
+ def get_options
740
+ result = {}
741
+ @chat.each do |m|
742
+ if m[:role] == 'option'
743
+ key, val = m[:content].split(' ', 2)
744
+ result[key] = val
745
+ end
746
+ end
747
+ result
748
+ end
749
+
750
+ def send_to_model(provider, api_key)
751
+ if provider == 'openai'
752
+ client = OpenAI::Client.new(api_key)
753
+ response = client.chat(messages: @chat)
754
+ @chat << { role: 'assistant', content: response }
755
+ end
756
+ end
757
+ end
758
+
759
+ processor = ChatProcessor.new(messages)
760
+ processor.remove_tool_messages
761
+ options = processor.get_options
762
+ processor.send_to_model('openai', ENV['OPENAI_API_KEY'])
763
+ ```
764
+
765
+ #### ✅ Idiomatic
766
+ ```ruby
767
+ chat = Chat.setup(messages)
768
+
769
+ # Use Chat's built-in DSL
770
+ chat.remove_role(:tool)
771
+
772
+ # Use Chat.options for option extraction
773
+ options = Chat.options(chat)
774
+
775
+ # Use the universal entry point
776
+ chat.ask(options.merge(endpoint: :nano))
777
+ ```
778
+
779
+ **Why the idiomatic version is better:**
780
+ - Uses the existing DSL (`remove_role`, `options`) instead of reimplementing.
781
+ - Delegates to `LLM.ask` which handles backend dispatch, caching, persistence.
782
+ - The Chat remains a plain Array — no wrapper object to maintain.
783
+ - Options are resolved through the full config cascade.
784
+
785
+ ---
786
+
787
+ ### Example 2: Building a multi-agent orchestration
788
+
789
+ #### ❌ Non-idiomatic
790
+ ```ruby
791
+ class Orchestrator
792
+ def initialize(question)
793
+ @question = question
794
+ @conversations = {}
795
+ end
796
+
797
+ def run
798
+ planner_messages = [{ role: 'user', content: @question }]
799
+ planner_messages << { role: 'system', content: 'You plan tasks.' }
800
+ planner_response = call_llm(planner_messages)
801
+
802
+ worker_messages = [{ role: 'user', content: planner_response }]
803
+ worker_messages << { role: 'system', content: 'You execute tasks.' }
804
+ worker_response = call_llm(worker_messages)
805
+
806
+ @conversations['planner'] = planner_messages
807
+ @conversations['worker'] = worker_messages
808
+
809
+ worker_response
810
+ end
811
+
812
+ def call_llm(messages)
813
+ # reimplement API call, caching, tool calling, etc.
814
+ client = OpenAI::Client.new
815
+ response = client.chat(parameters: { messages: messages })
816
+ response.dig('choices', 0, 'message', 'content')
817
+ end
818
+ end
819
+ ```
820
+
821
+ #### ✅ Idiomatic
822
+ ```ruby
823
+ require 'scout-ai'
824
+
825
+ module Orchestration
826
+ extend Workflow
827
+ self.include_workflow AgentWorkflow
828
+
829
+ input :chat, :text, 'Chat input'
830
+ extension :chat
831
+
832
+ chat_task :ask do
833
+ agent = self.agent(nil, chat: chat)
834
+
835
+ agent.start_chat.system "You are an orchestrator."
836
+
837
+ # Delegate to specialists via socialize
838
+ agent.socialize
839
+
840
+ agent.user "Plan and execute this request using specialists."
841
+ agent.chat
842
+ end
843
+ end
844
+ ```
845
+
846
+ Or using direct delegation:
847
+
848
+ ```ruby
849
+ chat_task :ask do
850
+ orchestrator = self.agent(nil, chat: chat)
851
+
852
+ planner = self.agent('Planner')
853
+ planner.user(orchestrator.answer)
854
+ plan = planner.chat
855
+
856
+ worker = self.agent('Worker')
857
+ worker.follow(plan)
858
+ worker.chat
859
+
860
+ log_agent(planner, 'planner')
861
+ log_agent(worker, 'worker')
862
+
863
+ worker.answer
864
+ end
865
+ ```
866
+
867
+ **Why the idiomatic version is better:**
868
+ - Uses `AgentWorkflow` mixin → gets `chat_task`, `helper :agent`, `helper :log_agent`.
869
+ - Uses `Agent` → gets stateful chats, tool wiring, start_chat loading.
870
+ - Uses `Chat` DSL → `.user`, `.system`, `.follow`, `.chat`, `.answer`.
871
+ - Uses `log_agent` → conversations are persisted for provenance.
872
+ - The entire strategy is a Scout Workflow → inputs are typed, jobs are cached,
873
+ dependencies are tracked, CLI integration is automatic.
874
+ - No reinvention of API calls, caching, or tool calling.
875
+
876
+ ---
877
+
878
+ ### Example 3: Adding a new message role
879
+
880
+ #### ❌ Non-idiomatic
881
+ ```ruby
882
+ # Adding a "context" role by modifying parse logic inline
883
+ def my_custom_parse(text)
884
+ messages = Chat.parse(text)
885
+ messages.each do |m|
886
+ if m[:content]&.start_with?('CONTEXT:')
887
+ m[:role] = 'context'
888
+ m[:content] = m[:content].sub('CONTEXT:', '').strip
889
+ end
890
+ end
891
+ messages
892
+ end
893
+ ```
894
+
895
+ #### ✅ Idiomatic
896
+ ```ruby
897
+ # Add the role to the Chat annotation DSL
898
+ module Chat
899
+ def context(content)
900
+ message(:context, content)
901
+ end
902
+ end
903
+
904
+ # Add handling in the chat file parser if needed (chat/parse.rb)
905
+ # Add processing in chat/process/ if compilation rules are needed
906
+
907
+ # Now it works everywhere:
908
+ chat = Chat.setup([])
909
+ chat.context("Some background info")
910
+ agent.context("Some background info") # via method_missing
911
+ ```
912
+
913
+ ---
914
+
915
+ ## 7. Summary: The Scout-AI Coding Mindset
916
+
917
+ 1. **Data is plain.** Chats are Arrays, options are Hashes. Annotate, don't wrap.
918
+ 2. **Compose, don't inherit.** Use modules, `extend`, `include`, `prepend`.
919
+ 3. **One abstraction, one responsibility.** Chat holds conversation. Agent holds state. Backend adapts to a provider. Tools define callable actions.
920
+ 4. **Convention discovers.** Directory structures and file names are the registry.
921
+ 5. **DSLs are methods on annotated objects.** Add methods to modules, get them everywhere via annotation and `method_missing`.
922
+ 6. **Use the full stack.** `IndiferentHash` for options, `Path` for files, `Scout::Config` for configuration, `Persist` for caching, `Log` for logging. Don't reimplement.
923
+ 7. **Keep it serializable.** Everything can be written to disk and read back. This is a feature, not a limitation.
924
+ 8. **Small files, clear boundaries.** `agent.rb` → `agent/chat.rb` → `agent/delegate.rb`. Each file adds one concern.
925
+
926
+ > **The guiding question when writing Scout-AI code:**
927
+ > *"Can I express this as a composition of existing abstractions, or does it
928
+ > need a new one? If new, is its boundary crisp?"*