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,810 @@
|
|
|
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
|
+
# 03 — Agent Class, Delegation Mechanics, and Socialization
|
|
10
|
+
|
|
11
|
+
> **Source files analysed**
|
|
12
|
+
> - `lib/scout/llm/agent.rb` (213 lines)
|
|
13
|
+
> - `lib/scout/llm/agent/chat.rb` (110 lines)
|
|
14
|
+
> - `lib/scout/llm/agent/delegate.rb` (323 lines)
|
|
15
|
+
> - `lib/scout/llm/agent/iterate.rb` (44 lines)
|
|
16
|
+
> - `lib/scout/llm/agent/workflow.rb` (workflow integration helper)
|
|
17
|
+
> - `lib/scout/llm/ask.rb` (entry-point `LLM.ask`)
|
|
18
|
+
> - `lib/scout/llm/chat/annotation.rb`, `chat/process/tools.rb`, `chat/process/clear.rb`, `chat/prompt.rb`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Agent Class Structure
|
|
23
|
+
|
|
24
|
+
### 1.1 Definition and composition
|
|
25
|
+
|
|
26
|
+
`LLM::Agent` is the central Ruby class that represents an autonomous AI agent.
|
|
27
|
+
It is defined in `lib/scout/llm/agent.rb` and extended by three mixin modules
|
|
28
|
+
loaded at the bottom of the file:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require_relative 'agent/chat'
|
|
32
|
+
require_relative 'agent/iterate'
|
|
33
|
+
require_relative 'agent/delegate'
|
|
34
|
+
require_relative 'agent/workflow'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each module adds a cohesive set of instance methods to the same `LLM::Agent`
|
|
38
|
+
class — a classic Ruby module-composition / "concern" pattern:
|
|
39
|
+
|
|
40
|
+
| Module file | Responsibility |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `agent/chat.rb` | Conversation lifecycle: `start`, `current_chat`, `chat`, `json`, etc. |
|
|
43
|
+
| `agent/iterate.rb` | Structured multi-step extraction (`iterate`, `iterate_dictionary`). |
|
|
44
|
+
| `agent/delegate.rb` | Multi-agent socialization & delegation (`socialize`, `delegate`, `ask_agent`). |
|
|
45
|
+
| `agent/workflow.rb` | Integration with Scout's `Workflow` system (`chat_task`, `AgentWorkflow`). |
|
|
46
|
+
|
|
47
|
+
The top-level file also defines two convenience module methods on `LLM`:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
def self.agent(...) = LLM::Agent.new(...) # factory shortcut
|
|
51
|
+
def self.load_agent(...) = LLM::Agent.load_agent(...) # discovery + loading
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 1.2 Initialization
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
def initialize(workflow: nil, knowledge_base: nil, start_chat: nil, **kwargs)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Parameter | Type | Purpose |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `workflow:` | `Workflow` module or name string | Scout workflow whose tasks become callable tools. If a `String`, it is resolved via `Workflow.require_workflow`. |
|
|
63
|
+
| `knowledge_base:` | `KnowledgeBase` | Optional knowledge base; its databases are exposed as tools. |
|
|
64
|
+
| `start_chat:` | `Chat` (Array of message hashes) | The seeded / system conversation that prefixes every new chat branch. |
|
|
65
|
+
| `**kwargs` | — | Captured into `@other_options` as an `IndiferentHash`. Typically holds `:model`, `:endpoint`, `:tools`, etc. |
|
|
66
|
+
|
|
67
|
+
### 1.3 Core attributes (attr\_accessor)
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
attr_accessor :workflow, :knowledge_base, :start_chat,
|
|
71
|
+
:process_exception, :other_options, :path, :job
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Additional attributes from mixins:
|
|
75
|
+
|
|
76
|
+
| Attribute | Defined in | Purpose |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `@society` | `delegate.rb` | Hash of `{agent_name => Agent}` templates loaded once and cloned per conversation. |
|
|
79
|
+
| `@chats` | `delegate.rb` | Hash of `{agent_name/conversation => Agent}` — the live specialist instances. |
|
|
80
|
+
| `@current_chat` | `chat.rb` (lazy via `current_chat`) | The active conversation (a `Chat`-annotated array). |
|
|
81
|
+
|
|
82
|
+
### 1.4 Lazy workflow creation
|
|
83
|
+
|
|
84
|
+
If no `@workflow` is set, one is created on demand:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
def workflow(&block)
|
|
88
|
+
if block_given?
|
|
89
|
+
# evaluate block in the workflow's context (DSL)
|
|
90
|
+
workflow.instance_eval &block
|
|
91
|
+
else
|
|
92
|
+
@workflow ||= begin
|
|
93
|
+
m = Module.new
|
|
94
|
+
m.extend Workflow
|
|
95
|
+
m.name ||= 'ScoutAgent'
|
|
96
|
+
m.tasks = {}
|
|
97
|
+
m
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This allows inline workflow definition in tests or scripts:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
agent.workflow do
|
|
107
|
+
task :my_task => :string do ... end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 2. The `ask` / `iterate` Loop
|
|
114
|
+
|
|
115
|
+
### 2.1 `Agent#ask` — entry point for inference
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
def ask(messages = nil, options = {})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Key behaviour:**
|
|
122
|
+
|
|
123
|
+
1. **Message resolution.** If `messages` is nil, uses `current_chat`. Normalises
|
|
124
|
+
to an array.
|
|
125
|
+
2. **Socialize hook.** If any message has `role: 'socialize'` and its content is
|
|
126
|
+
truthy (`true`, `T`, `1`), calls `self.socialize(options.dup)` — wiring up
|
|
127
|
+
the `ask` tool so the LLM can delegate.
|
|
128
|
+
3. **Tool merging.** Merges three layers of tool definitions:
|
|
129
|
+
- Explicit `options[:tools]`
|
|
130
|
+
- `@other_options[:tools]` (e.g. tools added by `socialize`/`delegate`)
|
|
131
|
+
- Workflow tools (`LLM.workflow_tools(workflow)`) and knowledge-base tools.
|
|
132
|
+
4. **Two execution paths:**
|
|
133
|
+
|
|
134
|
+
**Path A — Workflow `ask` task (preferred for agent-backed workflows):**
|
|
135
|
+
```ruby
|
|
136
|
+
if workflow && workflow.tasks.include?(:ask) && !no_ask_override
|
|
137
|
+
job = workflow.job(:ask, chat: Chat.print(messages))
|
|
138
|
+
job.produce
|
|
139
|
+
messages = Chat.project(job.short_path, LLM.chat(job.path))
|
|
140
|
+
```
|
|
141
|
+
The agent dispatches through the workflow's own `ask` task (a `chat_task`),
|
|
142
|
+
gaining Scout's job caching, provenance, and dependency system.
|
|
143
|
+
|
|
144
|
+
**Path B — Direct `LLM.ask`:**
|
|
145
|
+
```ruby
|
|
146
|
+
LLM.ask messages, @other_options.merge(log_errors: true).merge(options).merge(agent: false)
|
|
147
|
+
```
|
|
148
|
+
Calls the backend directly without going through a workflow job.
|
|
149
|
+
|
|
150
|
+
5. **Exception handling.** Wraps everything in a `begin/rescue`; if
|
|
151
|
+
`@process_exception` is a `Proc`, it is called with the exception and may
|
|
152
|
+
trigger a `retry`.
|
|
153
|
+
|
|
154
|
+
### 2.2 The multi-turn tool-calling loop (backend level)
|
|
155
|
+
|
|
156
|
+
The iterative tool-calling loop does **not** live in `Agent` itself — it lives
|
|
157
|
+
in the backend layer (`LLM::Backend::Default#chain_tools`):
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
def chain_tools(messages, output, tools, options = {}, &block)
|
|
161
|
+
if output.last[:role] == 'function_call_output'
|
|
162
|
+
# re-call ask with the tool output appended
|
|
163
|
+
output + ask(messages + output, options.except(:tool_choice).merge(return_messages: true), &block)
|
|
164
|
+
else
|
|
165
|
+
output # no pending tool call — done
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is **recursion**: each backend `ask` call checks whether the model emitted
|
|
171
|
+
a `function_call_output`; if so, it calls `ask` again with the growing message
|
|
172
|
+
list. The loop terminates when the model's last message is a plain `assistant`
|
|
173
|
+
message rather than a tool call.
|
|
174
|
+
|
|
175
|
+
**Iteration limits** are enforced via the prompt shortening system
|
|
176
|
+
(`lib/scout/llm/chat/prompt.rb`):
|
|
177
|
+
|
|
178
|
+
| Constant | Default | Meaning |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `DEFAULT_MAX_TOOL_CALLS` | 40 | Maximum number of tool call/output pairs retained in the prompt. |
|
|
181
|
+
| `DEFAULT_FULL_TOOL_CALLS` | 0 | Number of most-recent tool calls kept at full fidelity. |
|
|
182
|
+
| `DEFAULT_FULL_TOOL_OUTPUTS` | 10 | Number of most-recent tool outputs kept at full fidelity. |
|
|
183
|
+
| `DEFAULT_MAX_TOOL_CHARS` | 100 000 | Character budget for tool outputs. |
|
|
184
|
+
|
|
185
|
+
Older tool calls/outputs beyond these limits are truncated or dropped, which
|
|
186
|
+
effectively bounds the conversation depth and prevents unbounded recursion.
|
|
187
|
+
|
|
188
|
+
### 2.3 `Agent#prompt`
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
def prompt(messages, options = {})
|
|
192
|
+
messages = LLM.chat messages if String === messages
|
|
193
|
+
messages = Chat.follow start_chat, messages # prefix with start_chat
|
|
194
|
+
ask messages, options
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Convenience method: parses a string as chat syntax, prepends the agent's
|
|
199
|
+
`start_chat`, then delegates to `ask`.
|
|
200
|
+
|
|
201
|
+
### 2.4 `Agent#iterate` (iterate.rb)
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
def iterate(prompt = nil, &block)
|
|
205
|
+
self.endpoint :responses
|
|
206
|
+
self.user prompt if prompt
|
|
207
|
+
obj = self.json_format({ ... "type": "object", "properties": { "content": { "type": "array", "items": {"type": "string" } } } ... })
|
|
208
|
+
self.option :format, :text
|
|
209
|
+
list = Hash === obj ? obj['content'] : obj
|
|
210
|
+
list.each &block
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A structured-extraction loop: sends a prompt, asks the model to return a JSON
|
|
215
|
+
array of strings, then iterates over each element calling the supplied block.
|
|
216
|
+
`iterate_dictionary` is the same pattern but returns a flat key/value hash.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 3. Agent Chat Management (agent/chat.rb)
|
|
221
|
+
|
|
222
|
+
### 3.1 The dual-chat model
|
|
223
|
+
|
|
224
|
+
Every `Agent` maintains two Chat objects:
|
|
225
|
+
|
|
226
|
+
| Chat | Variable | Purpose |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| **Start chat** | `@start_chat` | Immutable seed messages (system instructions, tool intros, files). Prefixes every new conversation. |
|
|
229
|
+
| **Current chat** | `@current_chat` | The live, evolving conversation. |
|
|
230
|
+
|
|
231
|
+
### 3.2 `start_chat` accessor
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
def start_chat
|
|
235
|
+
@start_chat ||= Chat.setup([])
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Defaults to an empty chat if none was provided at construction.
|
|
240
|
+
|
|
241
|
+
### 3.3 `start` — creating a new conversation branch
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
def start(chat = nil)
|
|
245
|
+
if chat
|
|
246
|
+
(@current_chat || start_chat).annotate chat unless Chat === chat
|
|
247
|
+
@current_chat = chat
|
|
248
|
+
else
|
|
249
|
+
start_chat_obj = self.start_chat
|
|
250
|
+
Chat.setup(start_chat_obj) unless Chat === start_chat_obj
|
|
251
|
+
@current_chat = start_chat_obj.branch # shallow copy via annotate(self.dup)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- With no argument: creates a **branch** (shallow copy) of `start_chat` and
|
|
257
|
+
assigns it to `@current_chat`.
|
|
258
|
+
- With an argument: adopts the provided chat as the current chat (annotating it
|
|
259
|
+
to ensure it behaves as a `Chat`).
|
|
260
|
+
|
|
261
|
+
### 3.4 `current_chat`
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
def current_chat
|
|
265
|
+
@current_chat ||= start
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Lazy: on first access it calls `start` to create the default branch.
|
|
270
|
+
|
|
271
|
+
### 3.5 `method_missing` — Chat proxy
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
def method_missing(name, ...)
|
|
275
|
+
current_chat.send(name, ...)
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Any method not defined on `Agent` is forwarded to `current_chat`. This means
|
|
280
|
+
calls like `agent.user("hi")`, `agent.system("...")`, `agent.option(:model,
|
|
281
|
+
"gpt-4")`, `agent.print` are all delegated to the underlying Chat object.
|
|
282
|
+
|
|
283
|
+
### 3.6 `chat` — one round-trip with history
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
def chat(options = {})
|
|
287
|
+
response = ask(current_chat, options.merge(return_messages: true))
|
|
288
|
+
if Array === response
|
|
289
|
+
current_chat.concat(response)
|
|
290
|
+
options[:return_messages] ? response : current_chat.answer
|
|
291
|
+
else
|
|
292
|
+
current_chat.push({role: :assistant, content: response})
|
|
293
|
+
response
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Calls `ask` with `return_messages: true`, appends the response messages to the
|
|
299
|
+
current chat, and returns either the full message list or just the answer text.
|
|
300
|
+
|
|
301
|
+
### 3.7 JSON helpers
|
|
302
|
+
|
|
303
|
+
`json` and `json_format` push a format constraint onto the chat, call `chat`,
|
|
304
|
+
parse the output as JSON, and restore the format. They provide structured
|
|
305
|
+
extraction.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 4. Socialization and Delegation (delegate.rb) — CRITICAL
|
|
310
|
+
|
|
311
|
+
This module (323 lines) implements Scout-AI's multi-agent architecture. It
|
|
312
|
+
allows one Agent to **socialize** (expose a generic `ask` tool to the LLM) or
|
|
313
|
+
**delegate** (create named `hand_off_to_*` tools for specific agents).
|
|
314
|
+
|
|
315
|
+
### 4.1 Constants and invariants
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
SOCIAL_INHERIT_MODES = %w[none tools conversation].freeze
|
|
319
|
+
SOCIAL_AGENT_NAME = /\A[a-z_.-]+\z/i
|
|
320
|
+
SOCIAL_CONVERSATION_NAME = /\A[a-z0-9][a-z0-9_.-]*\z/i
|
|
321
|
+
SOCIAL_PRIVATE_OPTIONS = %i[
|
|
322
|
+
agent client current_meta format messages no_ask_override
|
|
323
|
+
previous_response_id process return_messages tool_choice tools
|
|
324
|
+
].freeze
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- **`SOCIAL_AGENT_NAME`** — valid agent name pattern (letters, dots,
|
|
328
|
+
underscores, hyphens).
|
|
329
|
+
- **`SOCIAL_CONVERSATION_NAME`** — conversation identifiers must start with
|
|
330
|
+
alphanumeric.
|
|
331
|
+
- **`SOCIAL_PRIVATE_OPTIONS`** — caller options that are **stripped** before
|
|
332
|
+
being passed to a specialist (prevents leaking session state, tool blocks,
|
|
333
|
+
or message arrays).
|
|
334
|
+
|
|
335
|
+
### 4.2 SOCIAL\_INHERIT\_MODES
|
|
336
|
+
|
|
337
|
+
These three modes control **how much caller context** flows to a specialist
|
|
338
|
+
when a new call or conversation is first created:
|
|
339
|
+
|
|
340
|
+
| Mode | What is inherited | Use case |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| **`none`** | Nothing. The specialist starts only with its own `start_chat`. | Fully isolated sub-agent. |
|
|
343
|
+
| **`tools`** *(default)* | Only the declarative tooling (roles: `introduce`, `tool`, `mcp`, `kb`) from the caller's current chat. | Give the specialist the same tool capabilities without conversation history. |
|
|
344
|
+
| **`conversation`** | The caller's entire current chat minus its own start\_chat prefix. | Full context sharing for deeply collaborative work. |
|
|
345
|
+
|
|
346
|
+
Implemented in `social_inherited_context`:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
def social_inherited_context(inherit)
|
|
350
|
+
case inherit
|
|
351
|
+
when 'none'
|
|
352
|
+
Chat.setup([])
|
|
353
|
+
when 'tools'
|
|
354
|
+
tooling = self.current_chat.tooling
|
|
355
|
+
social_chat_copy(tooling)
|
|
356
|
+
when 'conversation'
|
|
357
|
+
social_caller_context
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 4.3 The `socialize` method
|
|
363
|
+
|
|
364
|
+
**Signature:**
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
def socialize(options = {})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**What it does:** Registers a single tool named `:ask` in `@other_options[:tools]`.
|
|
371
|
+
When the LLM invokes this tool, it can ask **any** specialist agent.
|
|
372
|
+
|
|
373
|
+
**Tool schema exposed to the model:**
|
|
374
|
+
|
|
375
|
+
| Parameter | Type | Required | Description |
|
|
376
|
+
|---|---|---|---|
|
|
377
|
+
| `agent` | string | ✅ | Name of the specialist agent. |
|
|
378
|
+
| `prompt` | string | ✅ | Plain-text prompt (one user message). |
|
|
379
|
+
| `conversation` | string | ❌ | Named conversation identifier. Omit for one-shot. Reuse to continue. |
|
|
380
|
+
| `inherit` | enum `[none, tools, conversation]` | ❌ (default `tools`) | Context policy for new calls/conversations only. |
|
|
381
|
+
|
|
382
|
+
**Tool block (executed when the LLM calls `ask`):**
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
block = Proc.new do |_name, parameters|
|
|
386
|
+
agent_name, prompt, conversation, inherit = social_tool_parameters(parameters)
|
|
387
|
+
ask_agent(agent_name, prompt,
|
|
388
|
+
conversation: conversation,
|
|
389
|
+
inherit: inherit,
|
|
390
|
+
options: social_options)
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The model **never sees** the specialist's `Chat` object — it receives only the
|
|
395
|
+
text answer. The block captures `social_options` (a deep-duplicated copy of the
|
|
396
|
+
caller's `other_options` minus private keys) in its closure.
|
|
397
|
+
|
|
398
|
+
### 4.4 The `ask_agent` method — the delegation engine
|
|
399
|
+
|
|
400
|
+
**Signature:**
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
def ask_agent(agent_name, prompt, conversation: nil, inherit: 'tools', options: {})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Flow:**
|
|
407
|
+
|
|
408
|
+
1. Validate `agent_name` and `inherit`.
|
|
409
|
+
2. Resolve the specialist instance:
|
|
410
|
+
- If `conversation` is nil → uses conversation key `'default'` (a
|
|
411
|
+
single persistent conversation per agent, effectively shared across
|
|
412
|
+
one-shot calls).
|
|
413
|
+
- If `conversation` is provided → uses that named conversation.
|
|
414
|
+
3. `agent.user(prompt)` — appends the prompt as a user message.
|
|
415
|
+
4. Returns the specialist `Agent` object (the caller's tool block then calls
|
|
416
|
+
`agent.chat` to get the text response, or the socialize block does this
|
|
417
|
+
internally).
|
|
418
|
+
|
|
419
|
+
> **Security note (from source comment):** `ask_agent` uses `agent.user(prompt)`
|
|
420
|
+
> rather than `agent.prompt(prompt)` because `prompt` parses String input as
|
|
421
|
+
> Scout chat-file syntax — a malicious or confused prompt containing `tool:`
|
|
422
|
+
> or `system:` directives could inject control messages or grant tools.
|
|
423
|
+
> `user` simply appends a single user-role message.
|
|
424
|
+
|
|
425
|
+
### 4.5 The `load_chat` method — conversation scoping
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
def load_chat(agent_name, options = {}, conversation = nil, inherit: 'tools')
|
|
429
|
+
key = social_chat_key(agent_name, conversation) # "Worker/work_A"
|
|
430
|
+
@chats[key] ||= start_social_chat(agent_name, options, inherit)
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Conversation keys are scoped by agent:** `Worker/work_A` and `Critic/work_A`
|
|
435
|
+
are completely independent conversations. The `@chats` hash persists specialist
|
|
436
|
+
instances across calls within the same caller agent.
|
|
437
|
+
|
|
438
|
+
`inherit` is only consulted **once** — when the conversation is first created.
|
|
439
|
+
Follow-up turns reuse the existing conversation with its accumulated history.
|
|
440
|
+
|
|
441
|
+
### 4.6 `start_social_chat` — the full initialization
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
def start_social_chat(agent_name, options, inherit)
|
|
445
|
+
template = load_agent(agent_name, options) # load specialist template
|
|
446
|
+
agent = clone_social_agent(template) # deep clone
|
|
447
|
+
initial_chat = social_chat_copy(agent.start_chat) # copy start chat
|
|
448
|
+
initial_chat.follow(social_inherited_context(inherit)) # append inherited ctx
|
|
449
|
+
agent.start_chat.follow(initial_chat) # set as new start_chat
|
|
450
|
+
agent
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
This means the specialist's `start_chat` is rebuilt as:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
[specialist's original start_chat] + [inherited context from caller]
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
So the specialist always gets its own system prompt first, then optionally the
|
|
461
|
+
caller's tools or full conversation.
|
|
462
|
+
|
|
463
|
+
### 4.7 `clone_social_agent` — template isolation
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
def clone_social_agent(template)
|
|
467
|
+
agent = template.clone
|
|
468
|
+
agent.start_chat = social_chat_copy(template.start_chat)
|
|
469
|
+
agent.other_options = IndiferentHash.setup(social_duplicate(template.other_options || {}))
|
|
470
|
+
agent.society = nil
|
|
471
|
+
agent.chats = nil
|
|
472
|
+
agent.instance_variable_set(:@current_chat, nil)
|
|
473
|
+
agent
|
|
474
|
+
end
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Every conversation gets a **fresh clone** of the loaded template, with its own
|
|
478
|
+
`start_chat`, `other_options`, and nilled-out `society`/`chats` (preventing
|
|
479
|
+
accidental cross-contamination of delegation state).
|
|
480
|
+
|
|
481
|
+
### 4.8 `load_agent` (instance method) — specialist loading
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
def load_agent(agent_name, options = {})
|
|
485
|
+
agent_name = normalize_social_agent_name(agent_name)
|
|
486
|
+
@society ||= {}
|
|
487
|
+
@society[agent_name] ||= LLM.load_agent(agent_name, social_agent_options(options))
|
|
488
|
+
end
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**One immutable template per specialist.** The template is loaded once and
|
|
492
|
+
cloned per-conversation. `social_agent_options` strips private options:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
def social_agent_options(options)
|
|
496
|
+
merged = defaults.merge(supplied)
|
|
497
|
+
SOCIAL_PRIVATE_OPTIONS.each { |name| merged.delete(name) }
|
|
498
|
+
merged
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 4.9 `social_caller_context` — extracting non-start-chat messages
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
def social_caller_context
|
|
506
|
+
current = current_chat || []
|
|
507
|
+
base = start_chat || []
|
|
508
|
+
base_ids = base.each_with_object({}) { |m, ids| ids[m.object_id] = true }
|
|
509
|
+
|
|
510
|
+
if current.any? { |m| base_ids[m.object_id] }
|
|
511
|
+
# Fast path: same Hash objects — reject by object_id
|
|
512
|
+
current.reject { |m| base_ids[m.object_id] }
|
|
513
|
+
else
|
|
514
|
+
# Fallback: prefix matching for separately parsed Chats
|
|
515
|
+
prefix = 0
|
|
516
|
+
limit = [current.length, base.length].min
|
|
517
|
+
prefix += 1 while prefix < limit && current[prefix] == base[prefix]
|
|
518
|
+
current.drop(prefix)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
This extracts the "new" messages — everything the caller has added beyond its
|
|
524
|
+
own `start_chat` — for `inherit: 'conversation'` mode.
|
|
525
|
+
|
|
526
|
+
### 4.10 The `chat` / `conversation` parameter semantics
|
|
527
|
+
|
|
528
|
+
The old `chat` parameter (from earlier versions) is silently accepted for
|
|
529
|
+
backward compatibility via `social_tool_parameters`:
|
|
530
|
+
|
|
531
|
+
| Legacy `chat` value | Maps to `conversation` | Maps to `inherit` |
|
|
532
|
+
|---|---|---|
|
|
533
|
+
| `'current'` | `'current'` | `'conversation'` |
|
|
534
|
+
| `''`, `'none'`, `'false'` | `nil` (one-shot) | `'none'` |
|
|
535
|
+
| any other name | that name | `'tools'` |
|
|
536
|
+
|
|
537
|
+
New code should use `conversation` and `inherit` as separate parameters.
|
|
538
|
+
|
|
539
|
+
### 4.11 The `delegate` method — named hand-off tools
|
|
540
|
+
|
|
541
|
+
**Signature:**
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
def delegate(agent, name, description, task_name = nil, &block)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**What it does:** Creates a tool named `hand_off_to_#{name}` (e.g.,
|
|
548
|
+
`hand_off_to_worker`) that delegates to a specific, pre-loaded `Agent` object.
|
|
549
|
+
|
|
550
|
+
**Default tool block:**
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
block ||= Proc.new do |_name, parameters|
|
|
554
|
+
message = parameters[:message]
|
|
555
|
+
new_conversation = parameters[:new_conversation]
|
|
556
|
+
agent.start if new_conversation # reset conversation
|
|
557
|
+
agent.user message
|
|
558
|
+
agent.chat # get response
|
|
559
|
+
end
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Tool schema:**
|
|
563
|
+
|
|
564
|
+
| Parameter | Type | Required | Description |
|
|
565
|
+
|---|---|---|---|
|
|
566
|
+
| `message` | string | ✅ | Message to pass to the agent. |
|
|
567
|
+
| `new_conversation` | boolean | ❌ (default false) | If true, erase history and start fresh. |
|
|
568
|
+
|
|
569
|
+
**Key difference from `socialize`:**
|
|
570
|
+
|
|
571
|
+
| Aspect | `socialize` | `delegate` |
|
|
572
|
+
|---|---|---|
|
|
573
|
+
| Agent name | Model chooses at call time (`agent` param) | Hard-coded at registration time |
|
|
574
|
+
| Tool name | `:ask` (single tool for all agents) | `hand_off_to_#{name}` (one tool per agent) |
|
|
575
|
+
| Custom block | No (fixed block) | Yes (caller can supply `&block`) |
|
|
576
|
+
| Conversation management | Named conversations via `conversation` param | Single conversation, resettable via `new_conversation` |
|
|
577
|
+
|
|
578
|
+
### 4.12 Deep-duplication via `social_duplicate`
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
def social_duplicate(value)
|
|
582
|
+
case value
|
|
583
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[social_duplicate(k)] = social_duplicate(v) }
|
|
584
|
+
when Array then value.collect { |item| social_duplicate(item) }
|
|
585
|
+
when String then value.dup
|
|
586
|
+
else value
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
A recursive deep-copy that avoids `Marshal.load/dump` — important because tool
|
|
592
|
+
blocks (Procs) cannot be marshalled but are simply passed by reference (they
|
|
593
|
+
fall into the `else` branch).
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 5. Agent Loading
|
|
598
|
+
|
|
599
|
+
### 5.1 `LLM::Agent.load_agent` — the class method
|
|
600
|
+
|
|
601
|
+
**Signature:**
|
|
602
|
+
|
|
603
|
+
```ruby
|
|
604
|
+
def self.load_agent(agent_name = nil, options = {})
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Resolution order** (first match wins):
|
|
608
|
+
|
|
609
|
+
1. **Direct file path.** If `agent_name` is a filename:
|
|
610
|
+
- If it's a directory containing `agent.rb` → `load` that file.
|
|
611
|
+
- If it's a `.rb` file → `load` it directly.
|
|
612
|
+
|
|
613
|
+
2. **Named agent discovery** (when `agent_name` is a name string):
|
|
614
|
+
```ruby
|
|
615
|
+
workflow_path = Scout.workflows[agent_name] # Scout workflows dir
|
|
616
|
+
agent_path = Scout.Agent[agent_name] # Scout Agent dir
|
|
617
|
+
agent_path = Scout.var.Agent[agent_name] unless agent_path.exists?
|
|
618
|
+
agent_path = Scout.chats.Agent[agent_name] unless agent_path.exists?
|
|
619
|
+
agent_path = Scout.chats[agent_name] unless agent_path.exists?
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
3. **Workflow resolution:**
|
|
623
|
+
- If `workflow_path` exists → `Workflow.require_workflow(agent_name)`.
|
|
624
|
+
- If `agent_path/workflow.rb` exists → load that file.
|
|
625
|
+
- If `agent_path/python/*.py` exists → load as a Python workflow via
|
|
626
|
+
`PythonWorkflow.load_directory`.
|
|
627
|
+
|
|
628
|
+
4. **Knowledge base resolution:**
|
|
629
|
+
- `agent_path/knowledge_base` → `KnowledgeBase.load`.
|
|
630
|
+
- Or `workflow_path/knowledge_base`.
|
|
631
|
+
|
|
632
|
+
5. **Start chat resolution:**
|
|
633
|
+
- `agent_path/start_chat` → `Chat.setup(LLM.chat(file))`.
|
|
634
|
+
- Or `workflow_path/start_chat`.
|
|
635
|
+
- Or, if the workflow has documentation, `[{role: 'introduce', content: workflow.name}]`.
|
|
636
|
+
|
|
637
|
+
### 5.2 The agent directory convention
|
|
638
|
+
|
|
639
|
+
A named agent is discovered as a directory that may contain:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
Agent/
|
|
643
|
+
Worker/
|
|
644
|
+
agent.rb # Ruby file defining the agent (loaded via `load`)
|
|
645
|
+
workflow.rb # Scout Workflow definition
|
|
646
|
+
knowledge_base/ # KnowledgeBase directory
|
|
647
|
+
start_chat # Initial chat in Scout chat-file syntax
|
|
648
|
+
python/ # Python workflow files (*.py)
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
The lookup chain `Scout.workflows → Scout.Agent → Scout.var.Agent →
|
|
652
|
+
Scout.chats.Agent → Scout.chats` provides multiple well-known locations.
|
|
653
|
+
|
|
654
|
+
### 5.3 `load_from_path` — struct-path-based loading
|
|
655
|
+
|
|
656
|
+
```ruby
|
|
657
|
+
def self.load_from_path(path, workflow: nil, knowledge_base: nil, chat: nil)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Used when you have a `Path` object (Scout's Pathwise extension) with
|
|
661
|
+
sub-paths: `path['workflow.rb']`, `path['knowledge_base']`,
|
|
662
|
+
`path['start_chat']`. Each is checked for existence and loaded if present.
|
|
663
|
+
|
|
664
|
+
### 5.4 Instance-level `load_agent` (delegate.rb)
|
|
665
|
+
|
|
666
|
+
The `delegate.rb` module defines an **instance method** `load_agent` that wraps
|
|
667
|
+
the class method with socialization-specific option filtering:
|
|
668
|
+
|
|
669
|
+
```ruby
|
|
670
|
+
def load_agent(agent_name, options = {})
|
|
671
|
+
@society[agent_name] ||= LLM.load_agent(agent_name, social_agent_options(options))
|
|
672
|
+
end
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
This shadows the class method within instances that have mixed in the delegate
|
|
676
|
+
module (which is always, since `delegate.rb` is always loaded).
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 6. Key Abstractions and Design Patterns
|
|
681
|
+
|
|
682
|
+
### 6.1 Module composition (Ruby concerns)
|
|
683
|
+
|
|
684
|
+
The four `agent/*.rb` files all reopen `LLM::Agent` and add methods. There is no
|
|
685
|
+
inheritance hierarchy — just flat module inclusion. This keeps each concern in
|
|
686
|
+
its own file while sharing `@other_options`, `@current_chat`, etc.
|
|
687
|
+
|
|
688
|
+
### 6.2 `method_missing` proxy to Chat
|
|
689
|
+
|
|
690
|
+
`Agent#method_missing` forwards unknown method calls to `current_chat`, making
|
|
691
|
+
`Agent` a transparent proxy for Chat operations. This is a deliberate DSL
|
|
692
|
+
choice: `agent.user(...)`, `agent.system(...)`, `agent.print`, etc. all "just
|
|
693
|
+
work" without explicit delegation methods.
|
|
694
|
+
|
|
695
|
+
### 6.3 `IndiferentHash` for option passing
|
|
696
|
+
|
|
697
|
+
Scout's `IndiferentHash` (symbol/string-indifferent access) is used everywhere
|
|
698
|
+
for `options` and `@other_options`, allowing both `:model` and `'model'` keys.
|
|
699
|
+
|
|
700
|
+
### 6.4 The start\_chat / current\_chat branch pattern
|
|
701
|
+
|
|
702
|
+
```
|
|
703
|
+
start_chat (immutable seed)
|
|
704
|
+
│
|
|
705
|
+
├── branch → current_chat (conversation A)
|
|
706
|
+
├── branch → current_chat (conversation B) [via start(chat)]
|
|
707
|
+
└── ...
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`Chat#branch` does `self.annotate(self.dup)` — a shallow copy. The `start_chat`
|
|
711
|
+
is the persistent prefix; `current_chat` is the working copy.
|
|
712
|
+
|
|
713
|
+
### 6.5 Template + clone pattern for multi-agent
|
|
714
|
+
|
|
715
|
+
```
|
|
716
|
+
@society (templates) @chats (live instances)
|
|
717
|
+
──────────────────── ──────────────────────
|
|
718
|
+
"Worker" → Agent (template) "Worker/default" → Agent (clone)
|
|
719
|
+
"Critic" → Agent (template) "Worker/analysis_1" → Agent (clone)
|
|
720
|
+
"Critic/default" → Agent (clone)
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Templates are loaded once (`LLM.load_agent`). Each named conversation gets a
|
|
724
|
+
deep clone (`clone_social_agent`) so their `start_chat`, `other_options`, and
|
|
725
|
+
conversation state are fully independent.
|
|
726
|
+
|
|
727
|
+
### 6.6 Inheritance modes as a flexibility knob
|
|
728
|
+
|
|
729
|
+
The three `SOCIAL_INHERIT_MODES` create a spectrum of coupling:
|
|
730
|
+
|
|
731
|
+
```
|
|
732
|
+
none → fully sandboxed specialist (no caller context)
|
|
733
|
+
tools → shared capabilities, private history (default)
|
|
734
|
+
conversation → shared everything (tightly coupled pair)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
This lets an orchestrator agent control how much context each specialist
|
|
738
|
+
receives on a per-call basis — the model itself can choose `inherit` per tool
|
|
739
|
+
invocation.
|
|
740
|
+
|
|
741
|
+
### 6.7 Scout chat-file syntax as a security boundary
|
|
742
|
+
|
|
743
|
+
`ask_agent` deliberately uses `agent.user(prompt)` instead of
|
|
744
|
+
`agent.prompt(prompt)` because `prompt` parses chat-file syntax, which could
|
|
745
|
+
allow prompt injection to escalate privileges (e.g., injecting `tool:` lines).
|
|
746
|
+
The `user` method only appends a single `user`-role message, making delegation
|
|
747
|
+
safe even with untrusted LLM-generated prompts.
|
|
748
|
+
|
|
749
|
+
### 6.8 Workflow integration via `chat_task`
|
|
750
|
+
|
|
751
|
+
`agent/workflow.rb` defines `Workflow#chat_task` which creates Scout workflow
|
|
752
|
+
tasks that:
|
|
753
|
+
|
|
754
|
+
1. Accept a `chat` input (Scout chat-file format).
|
|
755
|
+
2. Load an agent via the `agent` helper.
|
|
756
|
+
3. Run the agent to completion.
|
|
757
|
+
4. Project the result back with `Chat.project(job.short_path, result)`.
|
|
758
|
+
5. Log delegated agent chats via `log_agent`.
|
|
759
|
+
|
|
760
|
+
This bridges the Agent abstraction into Scout's dependency-tracked,
|
|
761
|
+
cacheable workflow execution model.
|
|
762
|
+
|
|
763
|
+
### 6.9 Context truncation as implicit iteration limiting
|
|
764
|
+
|
|
765
|
+
Rather than a hard loop counter, the system bounds multi-turn depth through
|
|
766
|
+
`Chat.shorten_tools` in `prompt.rb`: tool calls/outputs beyond
|
|
767
|
+
`MAX_TOOL_CALLS` (40) are truncated, and those beyond `MAX_TOOL_OUTPUTS` are
|
|
768
|
+
dropped. This naturally constrains the context window and indirectly limits how
|
|
769
|
+
many tool-call rounds a conversation can sustain before the model "forgets"
|
|
770
|
+
earlier tool outputs.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Appendix: Method Reference Table
|
|
775
|
+
|
|
776
|
+
### Public methods added by each module
|
|
777
|
+
|
|
778
|
+
| Method | Source | Purpose |
|
|
779
|
+
|---|---|---|
|
|
780
|
+
| `ask(messages, options)` | `agent.rb` | Core inference entry point. |
|
|
781
|
+
| `prompt(messages, options)` | `agent.rb` | Parse string as chat, prefix with start\_chat, then `ask`. |
|
|
782
|
+
| `start(chat)` | `chat.rb` | Create/reset the current conversation. |
|
|
783
|
+
| `current_chat` | `chat.rb` | Lazy accessor for the active conversation. |
|
|
784
|
+
| `chat(options)` | `chat.rb` | One round-trip, appending to current\_chat. |
|
|
785
|
+
| `respond(...)` | `chat.rb` | Alias for `ask(current_chat, ...)`. |
|
|
786
|
+
| `json(...)` / `json_format(...)` | `chat.rb` | Structured JSON extraction. |
|
|
787
|
+
| `iterate(prompt, &block)` | `iterate.rb` | Extract a JSON array, iterate over it. |
|
|
788
|
+
| `iterate_dictionary(prompt, &block)` | `iterate.rb` | Extract a JSON dict, traverse it. |
|
|
789
|
+
| `socialize(options)` | `delegate.rb` | Register the generic `ask` tool for multi-agent delegation. |
|
|
790
|
+
| `delegate(agent, name, desc, &block)` | `delegate.rb` | Register a named `hand_off_to_*` tool. |
|
|
791
|
+
| `ask_agent(name, prompt, ...)` | `delegate.rb` | Programmatic delegation to a specialist. |
|
|
792
|
+
| `load_agent(name, options)` | `delegate.rb` | Load (or retrieve cached) specialist template. |
|
|
793
|
+
| `load_chat(name, options, conv, inherit:)` | `delegate.rb` | Get/create a named specialist conversation. |
|
|
794
|
+
|
|
795
|
+
### Private methods in delegate.rb
|
|
796
|
+
|
|
797
|
+
| Method | Purpose |
|
|
798
|
+
|---|---|
|
|
799
|
+
| `normalize_social_agent_name` | Validate agent name against regex. |
|
|
800
|
+
| `normalize_social_conversation_name` | Validate conversation name. |
|
|
801
|
+
| `normalize_social_inherit` | Validate inherit mode. |
|
|
802
|
+
| `social_chat_key` | Build `"agent/conversation"` key. |
|
|
803
|
+
| `social_agent_options` | Merge + strip private options for specialist. |
|
|
804
|
+
| `social_duplicate` | Recursive deep-copy (Hash/Array/String). |
|
|
805
|
+
| `social_chat_copy` | Deep-copy a Chat array. |
|
|
806
|
+
| `clone_social_agent` | Clone a template agent with isolated state. |
|
|
807
|
+
| `start_social_chat` | Create a new specialist conversation with inherited context. |
|
|
808
|
+
| `social_caller_context` | Extract non-start\_chat messages from current chat. |
|
|
809
|
+
| `social_inherited_context` | Resolve context based on inherit mode. |
|
|
810
|
+
| `social_tool_parameters` | Parse tool-call parameters, handle legacy `chat` arg. |
|