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.
- checksums.yaml +4 -4
- data/.vimproject +138 -50
- data/README.md +171 -290
- data/Rakefile +17 -1
- data/VERSION +1 -1
- data/doc/Improvements.md +325 -0
- data/doc/StartHere.md +110 -0
- data/doc/developer/Architecture.md +126 -0
- data/doc/developer/Backends.md +199 -0
- data/doc/developer/ChatLifecycle.md +183 -0
- data/doc/developer/DelegationInternals.md +295 -0
- data/doc/developer/DesignPrinciples.md +245 -0
- data/doc/developer/PromptProcessing.md +292 -0
- data/doc/developer/Provenance.md +317 -0
- data/doc/user/BuildingAgents.md +345 -0
- data/doc/user/Cookbook.md +333 -0
- data/doc/user/CoreConcepts.md +181 -0
- data/doc/user/Delegation.md +191 -0
- data/doc/user/GettingStarted.md +159 -0
- data/doc/user/ManagingContext.md +163 -0
- data/doc/user/MultiAgentWorkflows.md +256 -0
- data/doc/user/Python.md +159 -0
- data/doc/user/RunningInference.md +200 -0
- data/doc/user/ToolCalling.md +193 -0
- data/doc/user/WritingChats.md +197 -0
- data/lib/scout/llm/agent/chat.rb +61 -11
- data/lib/scout/llm/agent/delegate.rb +274 -65
- data/lib/scout/llm/agent/iterate.rb +2 -2
- data/lib/scout/llm/agent/save.rb +273 -0
- data/lib/scout/llm/agent/workflow.rb +164 -0
- data/lib/scout/llm/agent.rb +86 -61
- data/lib/scout/llm/ask.rb +62 -17
- data/lib/scout/llm/backends/anthropic.rb +9 -2
- data/lib/scout/llm/backends/bedrock.rb +15 -3
- data/lib/scout/llm/backends/default.rb +183 -99
- data/lib/scout/llm/backends/glm.rb +58 -0
- data/lib/scout/llm/backends/huggingface.rb +196 -26
- data/lib/scout/llm/backends/ollama.rb +13 -1
- data/lib/scout/llm/backends/openai.rb +0 -2
- data/lib/scout/llm/backends/openwebui.rb +20 -13
- data/lib/scout/llm/backends/relay.rb +22 -22
- data/lib/scout/llm/backends/responses.rb +1 -1
- data/lib/scout/llm/chat/agent_meta.rb +264 -0
- data/lib/scout/llm/chat/annotation.rb +39 -10
- data/lib/scout/llm/chat/parse.rb +28 -6
- data/lib/scout/llm/chat/persist.rb +25 -0
- data/lib/scout/llm/chat/process/clear.rb +41 -6
- data/lib/scout/llm/chat/process/files.rb +21 -6
- data/lib/scout/llm/chat/process/meta.rb +421 -34
- data/lib/scout/llm/chat/process/options.rb +21 -1
- data/lib/scout/llm/chat/process/tools.rb +56 -15
- data/lib/scout/llm/chat/process.rb +4 -0
- data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
- data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
- data/lib/scout/llm/chat/prompt.rb +48 -0
- data/lib/scout/llm/chat/provenance.rb +775 -0
- data/lib/scout/llm/chat/tool_calls.rb +76 -0
- data/lib/scout/llm/chat.rb +18 -2
- data/lib/scout/llm/embed.rb +11 -3
- data/lib/scout/llm/image.rb +86 -0
- data/lib/scout/llm/mcp.rb +10 -2
- data/lib/scout/llm/rag.rb +3 -3
- data/lib/scout/llm/tools/call.rb +160 -11
- data/lib/scout/llm/tools/knowledge_base.rb +1 -1
- data/lib/scout/llm/tools/workflow.rb +32 -16
- data/lib/scout/model/python/huggingface/causal.rb +23 -5
- data/lib/scout/model/python/huggingface.rb +2 -1
- data/lib/scout-ai.rb +1 -0
- data/python/README.md +197 -14
- data/python/scout_ai/huggingface/eval.py +245 -34
- data/python/tests/test_huggingface_eval.py +58 -0
- data/research/ChatAnalyst-required-changes.md +167 -0
- data/research/agent-delegation-analysis.md +810 -0
- data/research/agent-meta-provenance-integration-plan.md +622 -0
- data/research/agent-workflow-analysis.md +1120 -0
- data/research/backends-analysis.md +836 -0
- data/research/chat-core-analysis.md +946 -0
- data/research/chatanalyst-provenance/00-baseline.md +30 -0
- data/research/chatanalyst-provenance/01-repo-map.md +60 -0
- data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
- data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
- data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
- data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
- data/research/chatanalyst-provenance/07-critic-review.md +25 -0
- data/research/chatanalyst-provenance/final-report.md +45 -0
- data/research/chatanalyst-provenance/resumption.md +37 -0
- data/research/coding-philosophy-analysis.md +928 -0
- data/research/commands-analysis.md +947 -0
- data/research/multi-agent-patterns-analysis.md +853 -0
- data/research/prompt-strategies-analysis.md +630 -0
- data/research/prov-verbosity-fix-notes.md +77 -0
- data/research/provenance-analysis.md +469 -0
- data/research/provenance-navigation-design.md +640 -0
- data/research/synthesis-report.md +487 -0
- data/research/tools-system-analysis.md +779 -0
- data/scout-ai.gemspec +100 -11
- data/scout_commands/agent/ask +13 -3
- data/scout_commands/agent/kb +2 -0
- data/scout_commands/llm/ask +11 -4
- data/scout_commands/llm/md +76 -0
- data/scout_commands/llm/process_queries +48 -0
- data/scout_commands/llm/prov +602 -0
- data/scout_commands/llm/word +71 -0
- data/scout_commands/workflow/mcp +43 -0
- data/share/word/reference.docx +0 -0
- data/test/etc/AI/mock.yaml +11 -0
- data/test/fixtures/backends/anthropic.json +19 -0
- data/test/fixtures/backends/anthropic_tool_use.json +24 -0
- data/test/fixtures/backends/bedrock.json +8 -0
- data/test/fixtures/backends/bedrock_embedding.json +3 -0
- data/test/fixtures/backends/bedrock_tool_use.json +17 -0
- data/test/fixtures/backends/ollama.json +16 -0
- data/test/fixtures/backends/ollama_tool_call.json +27 -0
- data/test/fixtures/backends/openai_chat.json +21 -0
- data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
- data/test/fixtures/backends/responses.json +33 -0
- data/test/fixtures/backends/responses_tool_call.json +28 -0
- data/test/integration/README.md +32 -0
- data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
- data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
- data/test/integration/scout/llm/backends/test_relay.rb +52 -0
- data/test/integration/scout/llm/test_infrastructure.rb +74 -0
- data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
- data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
- data/test/integration/scout/model/test_base.rb +91 -0
- data/test/scout/llm/agent/test_chat.rb +8 -2
- data/test/scout/llm/agent/test_save.rb +413 -0
- data/test/scout/llm/agent/test_workflow.rb +110 -0
- data/test/scout/llm/backends/test_anthropic.rb +93 -10
- data/test/scout/llm/backends/test_bedrock.rb +118 -2
- data/test/scout/llm/backends/test_huggingface.rb +137 -42
- data/test/scout/llm/backends/test_ollama.rb +70 -20
- data/test/scout/llm/backends/test_openwebui.rb +42 -40
- data/test/scout/llm/backends/test_relay.rb +4 -2
- data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
- data/test/scout/llm/chat/process/test_meta.rb +518 -0
- data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
- data/test/scout/llm/chat/test_agent_meta.rb +357 -0
- data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
- data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
- data/test/scout/llm/chat/test_parse.rb +70 -15
- data/test/scout/llm/chat/test_prov_cli.rb +274 -0
- data/test/scout/llm/chat/test_provenance.rb +240 -0
- data/test/scout/llm/chat/test_tool_calls.rb +38 -0
- data/test/scout/llm/test_agent.rb +13 -36
- data/test/scout/llm/test_ask.rb +75 -52
- data/test/scout/llm/test_chat.rb +107 -13
- data/test/scout/llm/test_embed.rb +48 -0
- data/test/scout/llm/test_rag.rb +23 -16
- data/test/scout/llm/test_tools.rb +12 -1
- data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
- data/test/scout/llm/tools/test_mcp.rb +5 -3
- data/test/scout/llm/tools/test_workflow.rb +23 -2
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
- data/test/scout/model/python/huggingface/test_causal.rb +9 -3
- data/test/scout/model/python/huggingface/test_classification.rb +11 -2
- data/test/scout/model/python/test_torch.rb +2 -0
- data/test/scout/model/python/torch/test_helpers.rb +4 -0
- data/test/scout/model/test_base.rb +4 -2
- data/test/support/availability.rb +231 -0
- data/test/support/fake_clients.rb +138 -0
- data/test/support/fixtures.rb +21 -0
- data/test/support/infrastructure_probes.rb +136 -0
- data/test/support/mock_backend.rb +215 -0
- data/test/test_helper.rb +32 -2
- metadata +99 -10
- data/doc/Agent.md +0 -327
- data/doc/Chat.md +0 -458
- data/doc/LLM.md +0 -340
- data/doc/RAG.md +0 -129
- data/scout_commands/documenter +0 -148
- data/test/scout/llm/backends/test_openai.rb +0 -192
- data/test/scout/llm/backends/test_responses.rb +0 -238
- 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?"*
|