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,1120 @@
|
|
|
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
|
+
# 04 — AgentWorkflow: The Agent-as-Workflow Bridge
|
|
10
|
+
|
|
11
|
+
> **Primary source file:** `lib/scout/llm/agent/workflow.rb` (170 lines)
|
|
12
|
+
> **Related source files:**
|
|
13
|
+
> - `lib/scout/llm/agent.rb` (constructor, `ask`, `workflow` method, `load_agent`)
|
|
14
|
+
> - `lib/scout/llm/agent/chat.rb` (`start_chat`, `current_chat`, `chat`)
|
|
15
|
+
> - `lib/scout/llm/agent/iterate.rb` (`iterate`, `iterate_dictionary`)
|
|
16
|
+
> - `lib/scout/llm/chat/persist.rb` (`.chat` extension registration, save/load drivers)
|
|
17
|
+
> - `lib/scout/llm/chat/process/meta.rb` (`Chat.project`, `Chat.serialize_meta`, `jobs`)
|
|
18
|
+
> - `lib/scout/llm/chat/process/tools.rb` (`Chat.allow_dir`, `Chat.allow_read_dir`)
|
|
19
|
+
> - `lib/scout/llm/chat/prompt.rb` (`full_tool_calls`, `max_tool_outputs`)
|
|
20
|
+
> - `lib/scout-gear/.../workflow/definition.rb` (`include_workflow`, `TYPE_EXTENSIONS`)
|
|
21
|
+
> - `lib/scout-gear/.../workflow/step/file.rb` (`files_dir`, `file`)
|
|
22
|
+
> - SC26 workflows: `Planned`, `Critic`, `Branched`, `Refined`, `InterpretData`
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. What AgentWorkflow Is
|
|
27
|
+
|
|
28
|
+
`AgentWorkflow` is a Scout **Workflow module** that provides a compact DSL for
|
|
29
|
+
declaring **chat tasks** — Scout workflow tasks whose inputs, outputs, and
|
|
30
|
+
persistence are all built around the `Chat` abstraction. When a Workflow module
|
|
31
|
+
writes `self.include_workflow AgentWorkflow`, it gains:
|
|
32
|
+
|
|
33
|
+
| Helper / construct | Kind | Purpose |
|
|
34
|
+
|------------------------|-----------------|-----------------------------------------------------------|
|
|
35
|
+
| `chat_task` | DSL declaration | Define a workflow task of type `:chat` with a chat input |
|
|
36
|
+
| `chat` | Instance helper | Parse or build the task's `Chat` from the `chat` input |
|
|
37
|
+
| `options` | Instance helper | Extract `LLM.options` from the current chat |
|
|
38
|
+
| `agent` | Instance helper | Factory: load an `LLM::Agent`, wire it to this job |
|
|
39
|
+
| `agent_options` | Instance helper | Strip private keys from an options hash |
|
|
40
|
+
| `tooling` | Instance helper | Extract tool/kb/mcp/introduce messages from the chat |
|
|
41
|
+
| `tooling_intro` | Instance helper | Subset of `tooling` containing only `introduce` messages |
|
|
42
|
+
| `log_agent` | Instance helper | Persist an agent's chats into the job's `log/` directory |
|
|
43
|
+
| `add_chat_dependencies`| Instance helper | Register chat-referenced jobs as Scout dependencies |
|
|
44
|
+
|
|
45
|
+
It also patches `Workflow.require_workflow` and `LLM::Agent` (see §10).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Module Structure and `include_workflow`
|
|
50
|
+
|
|
51
|
+
### 2.1 Definition
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
module AgentWorkflow
|
|
55
|
+
extend Workflow # ← it is itself a Workflow module
|
|
56
|
+
|
|
57
|
+
helper :chat do |chat=nil| ... end
|
|
58
|
+
helper :options do ... end
|
|
59
|
+
helper :agent do |name=nil, chat:nil, options:nil, tooling:nil, files:nil, **kwargs| ... end
|
|
60
|
+
helper :log_agent do |agent, agent_name=nil| ... end
|
|
61
|
+
# ... other helpers
|
|
62
|
+
|
|
63
|
+
# Plus the chat_task definition patched into Workflow (see §3)
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2.2 How `include_workflow` extends a host module
|
|
68
|
+
|
|
69
|
+
`Workflow#include_workflow` (defined in `scout-gear/lib/scout/workflow/definition.rb`,
|
|
70
|
+
line 233) performs a **five-step merge**:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
def include_workflow(workflow)
|
|
74
|
+
workflow.documentation
|
|
75
|
+
self.asynchronous_exports += workflow.asynchronous_exports
|
|
76
|
+
self.synchronous_exports += workflow.synchronous_exports
|
|
77
|
+
self.exec_exports += workflow.exec_exports
|
|
78
|
+
self.stream_exports += workflow.stream_exports
|
|
79
|
+
self.tasks.merge! workflow.tasks # ← merge task definitions
|
|
80
|
+
self.tasks.each{|_,t| t.workflow = workflow }
|
|
81
|
+
self.helpers.merge! workflow.helpers # ← merge helper definitions
|
|
82
|
+
self.include workflow # ← Ruby module inclusion
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Effect:** all helpers defined inside `AgentWorkflow` become callable inside
|
|
87
|
+
the host module's task blocks (`self.agent`, `self.chat`, etc.). The host does
|
|
88
|
+
not need to re-define any of them.
|
|
89
|
+
|
|
90
|
+
### 2.3 Canonical usage pattern
|
|
91
|
+
|
|
92
|
+
Every SC26 pipeline workflow follows the same two-line preamble:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
module Planned
|
|
96
|
+
extend Workflow
|
|
97
|
+
self.include_workflow AgentWorkflow # ← gains all helpers + chat_task
|
|
98
|
+
# ...
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 3. `chat_task` — The Core DSL Construct
|
|
105
|
+
|
|
106
|
+
### 3.1 Declaration syntax
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
chat_task :task_name do
|
|
110
|
+
# ... arbitrary Ruby that builds and returns an Agent or Hash
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is defined not inside the `AgentWorkflow` module body but as a **patch on
|
|
115
|
+
`Module`** (via `class << self` inside the `Workflow` module reopening):
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
module Workflow
|
|
119
|
+
def chat_task(task_name, &block)
|
|
120
|
+
input :chat, :text, 'Chat in Scout-AI chat-file format'
|
|
121
|
+
task task_name => :chat do |chat|
|
|
122
|
+
# ... execution body (see §3.3)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Because `Workflow` is reopened at the class level, `chat_task` is available to
|
|
129
|
+
**every** Workflow module that has `require`'d the agent library, not just those
|
|
130
|
+
that explicitly `include_workflow AgentWorkflow`. However, to use `self.agent`
|
|
131
|
+
and the other helpers inside the block, `include_workflow AgentWorkflow` (or
|
|
132
|
+
equivalent) is required.
|
|
133
|
+
|
|
134
|
+
### 3.2 Differences from a regular `task`
|
|
135
|
+
|
|
136
|
+
| Aspect | `task :name => :type do ... end` | `chat_task :name do ... end` |
|
|
137
|
+
|------------------------|---------------------------------------|--------------------------------------------------|
|
|
138
|
+
| **Input** | Manually declared with `input` | Automatically declares `input :chat, :text, ...` |
|
|
139
|
+
| **Type** | Any registered type (`:string`, `:tsv`, etc.) | Always `:chat` |
|
|
140
|
+
| **File extension** | Determined by `TYPE_EXTENSIONS[type]` | Always `.chat` (registered by `persist.rb`) |
|
|
141
|
+
| **Return processing** | Raw return value is the result | `LLM::Agent` → auto-processed via `agent.chat` or differential; `Hash` → wrapped; error → JSON error message |
|
|
142
|
+
| **Provenance** | Manual | `Chat.project(self.short_path, result)` called automatically |
|
|
143
|
+
| **Agent logging** | Manual | `log_agent(agent)` called automatically when the block returns an Agent |
|
|
144
|
+
| **Error handling** | Propagates exceptions | `ScoutException` → graceful error message in chat format |
|
|
145
|
+
|
|
146
|
+
### 3.3 The full execution body (annotated)
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
task task_name => :chat do |chat|
|
|
150
|
+
begin
|
|
151
|
+
# 1. Execute the user's block. The block can return:
|
|
152
|
+
# - an LLM::Agent (most common)
|
|
153
|
+
# - a Hash (single message)
|
|
154
|
+
# - an Array (raw message list)
|
|
155
|
+
# - anything else (returned as-is)
|
|
156
|
+
response = self.instance_exec(&block)
|
|
157
|
+
|
|
158
|
+
# 2. Normalize the response into a message array
|
|
159
|
+
result = if LLM::Agent === response
|
|
160
|
+
agent = response
|
|
161
|
+
# If the last message is from the user, the model hasn't responded yet.
|
|
162
|
+
# Run the agent's chat to get the assistant's response.
|
|
163
|
+
if agent.current_chat.last[:role].to_s == 'user'
|
|
164
|
+
agent.chat(return_messages: true)
|
|
165
|
+
else
|
|
166
|
+
# Already responded; extract only the messages after start_chat
|
|
167
|
+
agent.current_chat - agent.start_chat
|
|
168
|
+
end.tap { log_agent(agent) } # ← persist agent chats to log/
|
|
169
|
+
|
|
170
|
+
elsif Hash === response
|
|
171
|
+
[response] # single message
|
|
172
|
+
|
|
173
|
+
else
|
|
174
|
+
response # pass through (Array, etc.)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# 3. Tag the result with a provenance meta-message and return
|
|
178
|
+
Chat.project(self.short_path, result)
|
|
179
|
+
|
|
180
|
+
rescue ScoutException
|
|
181
|
+
# 4. On expected failure, produce an error message in chat format
|
|
182
|
+
error = { role: :assistant,
|
|
183
|
+
content: { exception: $!, job: self.short_path }.to_json }
|
|
184
|
+
Chat.project(self.short_path, [error])
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 3.4 Key design decisions
|
|
190
|
+
|
|
191
|
+
1. **Block return value determines behavior.** Returning an `Agent` triggers the
|
|
192
|
+
full lifecycle (auto-run, log, project). Returning raw data is also supported.
|
|
193
|
+
|
|
194
|
+
2. **Lazy execution.** If the block sets up an agent but the last message is
|
|
195
|
+
still `:user`, `chat_task` calls `agent.chat(return_messages: true)` to
|
|
196
|
+
trigger inference. If the block already ran the agent (e.g., `agent.json`),
|
|
197
|
+
the last message is `:assistant` and the differential is returned directly.
|
|
198
|
+
|
|
199
|
+
3. **Automatic provenance.** Every result is wrapped with `Chat.project`, which
|
|
200
|
+
prepends a `{:role => :meta, :content => "job=<short_path>"}` marker.
|
|
201
|
+
|
|
202
|
+
4. **Graceful degradation.** `ScoutException` (the agent-level exception class)
|
|
203
|
+
is caught and converted to a JSON error message, not propagated as a crash.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 4. The `chat` Helper
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
helper :chat do |chat=nil|
|
|
211
|
+
@chat ||= begin
|
|
212
|
+
chat = recursive_inputs[:chat] # pull from recursive input chain
|
|
213
|
+
chat = Chat.parse(chat) if String === chat
|
|
214
|
+
Chat.setup(chat)
|
|
215
|
+
chat
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Behavior:**
|
|
221
|
+
- Memoized (`@chat ||= ...`): the chat is parsed once and reused.
|
|
222
|
+
- Pulls the raw `chat` input from `recursive_inputs`, which is Scout's mechanism
|
|
223
|
+
for passing inputs down a dependency chain. In `chat_task`, this is the `chat`
|
|
224
|
+
parameter value.
|
|
225
|
+
- If the value is a String, it is parsed via `Chat.parse` (YAML/markdown →
|
|
226
|
+
message array). If already a Chat (or Array), it is set up in-place.
|
|
227
|
+
- Returns a `Chat` instance (an Array annotated with the `Chat` module).
|
|
228
|
+
|
|
229
|
+
**Usage in task blocks:**
|
|
230
|
+
```ruby
|
|
231
|
+
chat_task :ask do
|
|
232
|
+
chat = self.chat # ← the parsed input chat
|
|
233
|
+
chat.follow step(:request).load # ← append dependency results
|
|
234
|
+
# ...
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 5. The `options` Helper
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
helper :options do
|
|
244
|
+
@options ||= LLM.options self.chat
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Extracts LLM configuration options (endpoint, model, parameters, etc.) from the
|
|
249
|
+
current chat. `LLM.options` reads these from special `:option` role messages
|
|
250
|
+
embedded in the chat (e.g., `{:role => :option, :content => "model gpt-4o"}`).
|
|
251
|
+
|
|
252
|
+
These options are typically passed to the `agent` helper to configure the
|
|
253
|
+
loaded agent.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 6. The `agent` Helper — The Factory Method
|
|
258
|
+
|
|
259
|
+
This is the most important helper. It loads (or retrieves a cached) `LLM::Agent`,
|
|
260
|
+
configures it with tooling, chat context, system messages, and files, and
|
|
261
|
+
returns it ready for use.
|
|
262
|
+
|
|
263
|
+
### 6.1 Signature
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
helper :agent do |name = nil, chat: nil, options: nil, tooling: nil,
|
|
267
|
+
files: nil, **kwargs|
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Parameter | Type | Default | Description |
|
|
271
|
+
|-------------|-----------------|----------------|---------------------------------------------------|
|
|
272
|
+
| `name` | String / Symbol / `nil` | `nil` | Agent name to load via `LLM.load_agent`. `nil` creates a bare agent. |
|
|
273
|
+
| `chat` | Chat / Array / `nil` | `nil` | Conversation context to follow into the agent's start_chat |
|
|
274
|
+
| `options` | Hash / `nil` | `self.options` | LLM options (model, endpoint, parameters, etc.) |
|
|
275
|
+
| `tooling` | Array / `nil` | `self.tooling` | Tool/introduce/kb/mcp messages to prepend |
|
|
276
|
+
| `files` | Array / `nil` | `nil` | File paths to attach to the agent's chat |
|
|
277
|
+
| `**kwargs` | varies | — | Merged into options via `IndiferentHash.add_defaults` |
|
|
278
|
+
|
|
279
|
+
### 6.2 What it does, step by step
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
# Step 1: Resolve defaults
|
|
283
|
+
options = self.options if options.nil? # inherit task's LLM options
|
|
284
|
+
tooling = self.tooling if tooling.nil? # inherit task's tooling
|
|
285
|
+
options = IndiferentHash.add_defaults options, kwargs # merge keyword args
|
|
286
|
+
|
|
287
|
+
# Step 2: Load the agent
|
|
288
|
+
agent = LLM.load_agent name, agent_options(options)
|
|
289
|
+
agent.job = self # link agent to this workflow step
|
|
290
|
+
|
|
291
|
+
# Step 3: Inject tooling
|
|
292
|
+
agent.start_chat.follow tooling if tooling && !tooling.empty?
|
|
293
|
+
|
|
294
|
+
# Step 4: System messages about job context
|
|
295
|
+
agent.start_chat.system "Your current working directory is #{Dir.pwd}."
|
|
296
|
+
agent.start_chat.system "You are working through an ask job with path #{self.path}..."
|
|
297
|
+
agent.start_chat.system "Tool call content may be truncated after #{Chat.full_tool_calls}, ..."
|
|
298
|
+
# (these let the agent know about its sandbox boundaries)
|
|
299
|
+
|
|
300
|
+
# Step 5: Dependency context (if the workflow has deps)
|
|
301
|
+
if dependencies.any?
|
|
302
|
+
agent.start_chat.system "This workflow job has the following dependencies:\n#{rec_dependencies...}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Step 6: Cross-job awareness
|
|
306
|
+
if chat && !chat.empty?
|
|
307
|
+
other_jobs = LLM.chat(chat.dup).jobs
|
|
308
|
+
if other_jobs.any?
|
|
309
|
+
agent.start_chat.system "There are other jobs found in this chat:\n#{other_jobs...}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Step 7: File attachments
|
|
314
|
+
files.each do |path|
|
|
315
|
+
target = file(path)
|
|
316
|
+
if File.exist?(target.find)
|
|
317
|
+
agent.start_chat.file target
|
|
318
|
+
elsif File.exist?(path.find)
|
|
319
|
+
agent.start_chat.file path
|
|
320
|
+
end
|
|
321
|
+
end if files
|
|
322
|
+
|
|
323
|
+
# Step 8: Inject conversation context (with filtering)
|
|
324
|
+
if chat && !chat.empty?
|
|
325
|
+
chat = LLM.chat(chat)
|
|
326
|
+
# Remove stale assignment/context messages from previous pipeline steps
|
|
327
|
+
chat.reject! { |msg| msg[:content].to_s.start_with? 'You have been assigned' }
|
|
328
|
+
chat.reject! { |msg| msg[:content].to_s.start_with? 'This workflow job has the following' }
|
|
329
|
+
chat.reject! { |msg| msg[:content].to_s.start_with? 'There are other jobs found in this chat' }
|
|
330
|
+
agent.start_chat.follow chat
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
agent
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 6.3 What it returns
|
|
337
|
+
|
|
338
|
+
An `LLM::Agent` instance with:
|
|
339
|
+
- `agent.job` set to the current `Step` (for provenance tracking)
|
|
340
|
+
- `agent.start_chat` populated with tooling, system messages, and context
|
|
341
|
+
- Ready for `.user(...)` / `.chat(...)` / `.json(...)` calls
|
|
342
|
+
|
|
343
|
+
### 6.4 The `agent_options` sub-helper
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
helper :agent_options do |options|
|
|
347
|
+
IndiferentHash.setup options.except(:agent, 'agent', :chat, 'chat')
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Strips `:agent` and `:chat` keys from the options hash before passing to
|
|
352
|
+
`LLM.load_agent`. This prevents infinite recursion (load_agent would try to
|
|
353
|
+
re-parse the chat input).
|
|
354
|
+
|
|
355
|
+
### 6.5 Context filtering — a subtle but important detail
|
|
356
|
+
|
|
357
|
+
When a chat is followed into the agent's start_chat, three categories of
|
|
358
|
+
messages are **stripped**:
|
|
359
|
+
|
|
360
|
+
1. `"You have been assigned"` — assignment prompts from prior pipeline steps
|
|
361
|
+
2. `"This workflow job has the following"` — dependency listings from prior steps
|
|
362
|
+
3. `"There are other jobs found in this chat"` — cross-job awareness messages
|
|
363
|
+
|
|
364
|
+
This prevents context bloat as chats flow through multi-step pipelines.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 7. The `tooling` and `tooling_intro` Helpers
|
|
369
|
+
|
|
370
|
+
### 7.1 `tooling`
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
helper :tooling do
|
|
374
|
+
@tooling ||= begin
|
|
375
|
+
chat = self.chat
|
|
376
|
+
chat.remove_role(:tool) +
|
|
377
|
+
chat.remove_role(:kb) +
|
|
378
|
+
chat.remove_role(:mcp) +
|
|
379
|
+
chat.remove_role(:introduce)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**What it does:** Extracts all configuration messages from the input chat that
|
|
385
|
+
define the agent's capabilities:
|
|
386
|
+
- `:tool` — tool definitions (JSON schemas)
|
|
387
|
+
- `:kb` — knowledge base definitions
|
|
388
|
+
- `:mcp` — Model Context Protocol server definitions
|
|
389
|
+
- `:introduce` — agent introduction/instruction messages
|
|
390
|
+
|
|
391
|
+
These are **removed** from the chat (mutating it) and **returned** as a combined
|
|
392
|
+
array. The memoization (`@tooling ||=`) means the extraction happens once.
|
|
393
|
+
|
|
394
|
+
**Effect:** The tooling messages are extracted so they can be selectively
|
|
395
|
+
re-injected into agents via the `agent` helper's `tooling:` parameter. This
|
|
396
|
+
allows different agents in the same pipeline to receive different subsets of
|
|
397
|
+
tools.
|
|
398
|
+
|
|
399
|
+
### 7.2 `tooling_intro`
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
helper :tooling_intro do
|
|
403
|
+
self.tooling.select { |msg| msg[:role] == 'introduce ' }
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Note the trailing space in `'introduce '`** — this is present in the source
|
|
408
|
+
code. This selects only the `introduce` role messages, which contain agent
|
|
409
|
+
instruction/context prose. It is used when an agent should receive the
|
|
410
|
+
introduction context but not the actual tool definitions (e.g., a Planner agent
|
|
411
|
+
that should understand the available tools conceptually but not call them).
|
|
412
|
+
|
|
413
|
+
**Typical usage in SC26:**
|
|
414
|
+
```ruby
|
|
415
|
+
# Planner gets intros only (no actual tools to call)
|
|
416
|
+
agent = self.agent :Planner, chat: chat, tooling: self.tooling_intro
|
|
417
|
+
|
|
418
|
+
# Worker gets full tooling (tools + intros)
|
|
419
|
+
agent = self.agent worker_agent, chat: chat, tooling: self.tooling
|
|
420
|
+
|
|
421
|
+
# Final report agent gets no tools at all
|
|
422
|
+
agent = self.agent :User, tooling: false, chat: chat
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 8. The `log_agent` Helper — Provenance Persistence
|
|
428
|
+
|
|
429
|
+
### 8.1 Signature and behavior
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
helper :log_agent do |agent, agent_name=nil|
|
|
433
|
+
dir = agent_name ? file('log')[agent_name] : file('log')
|
|
434
|
+
|
|
435
|
+
agent.chats.each do |name, other|
|
|
436
|
+
dir.society[name].set_extension('chat').write other.current_chat.print
|
|
437
|
+
#add_chat_dependencies(other.current_chat) # ← commented out
|
|
438
|
+
end if agent.chats
|
|
439
|
+
|
|
440
|
+
dir['agent.chat'].write agent.current_chat.print
|
|
441
|
+
#add_chat_dependencies(agent.current_chat) # ← commented out
|
|
442
|
+
|
|
443
|
+
update_info :dependencies, dependencies.collect { |dependency| dependency.path.find }
|
|
444
|
+
agent
|
|
445
|
+
end
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 8.2 What it persists
|
|
449
|
+
|
|
450
|
+
| File written | Content |
|
|
451
|
+
|--------------------------------------|---------------------------------------------------|
|
|
452
|
+
| `<files_dir>/log/agent.chat` | The agent's full current_chat (all messages) |
|
|
453
|
+
| `<files_dir>/log/<agent_name>/agent.chat` | Same, but under a named subdirectory |
|
|
454
|
+
| `<files_dir>/log/<agent_name>/society/<delegated_name>.chat` | For each agent in the society hash, its current chat |
|
|
455
|
+
|
|
456
|
+
### 8.3 Provenance trail
|
|
457
|
+
|
|
458
|
+
These persisted `.chat` files are the **evidence trail** for what an agent did.
|
|
459
|
+
They are discoverable by:
|
|
460
|
+
- `Chat.job_agent_chat_files(job)` — globs `job.file('log').glob('**/*.chat')`
|
|
461
|
+
- The `ChatAnalyst` agent's `Session.discover_job` method
|
|
462
|
+
- The `scout llm prov` command
|
|
463
|
+
|
|
464
|
+
### 8.4 `add_chat_dependencies` (currently commented out)
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
helper :add_chat_dependencies do |chat|
|
|
468
|
+
chat.jobs.each do |job_path|
|
|
469
|
+
next if job_path.to_s == self.short_path.to_s
|
|
470
|
+
next if dependencies.find{|dep| dep.path.find == job_path.find }
|
|
471
|
+
begin
|
|
472
|
+
job = Step.load(job_path)
|
|
473
|
+
dependencies << job unless dependencies.select{|dep| dep.path}.include?(job)
|
|
474
|
+
rescue
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This would register any jobs referenced in the chat's meta messages as Scout
|
|
481
|
+
dependencies of the current step. The calls are commented out in the current
|
|
482
|
+
source, meaning cross-job dependency links are **not** automatically
|
|
483
|
+
established at present. The `update_info` call at the end of `log_agent` still
|
|
484
|
+
writes the dependency list to the step's info file.
|
|
485
|
+
|
|
486
|
+
### 8.5 Auto-invocation
|
|
487
|
+
|
|
488
|
+
`log_agent` is called **automatically** by `chat_task` when the block returns an
|
|
489
|
+
`LLM::Agent` (see §3.3, the `.tap { log_agent(agent) }` call). You rarely call
|
|
490
|
+
it manually — unless you are managing multiple agents in a single task (as the
|
|
491
|
+
`Branched` workflow does).
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 9. The `.chat` Extension and Persistence
|
|
496
|
+
|
|
497
|
+
### 9.1 Registration
|
|
498
|
+
|
|
499
|
+
`lib/scout/llm/chat/persist.rb` registers the `.chat` extension:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
Workflow::TYPE_EXTENSIONS[:chat] = :chat
|
|
503
|
+
|
|
504
|
+
Persist.save_drivers[:chat] = proc do |file, content|
|
|
505
|
+
case content
|
|
506
|
+
when LLM::Agent
|
|
507
|
+
new_chat = content.current_chat - content.start_chat
|
|
508
|
+
Open.sensible_write(file, LLM.print(new_chat))
|
|
509
|
+
when Array
|
|
510
|
+
Open.sensible_write(file, LLM.print(content))
|
|
511
|
+
else
|
|
512
|
+
# ... stream handling
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
Persist.load_drivers[:chat] = proc do |file|
|
|
517
|
+
String === file ? LLM.chat(file) : file
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 9.2 How chat task results are saved and loaded
|
|
522
|
+
|
|
523
|
+
1. **On save** (`Persist.save_drivers[:chat]`):
|
|
524
|
+
- If the result is an `LLM::Agent`: saves `current_chat - start_chat` (only the
|
|
525
|
+
new messages, not the preamble).
|
|
526
|
+
- If the result is an Array: saves all messages.
|
|
527
|
+
- Uses `Open.sensible_write` (atomic write with temp file + rename).
|
|
528
|
+
- Serialization format is `LLM.print(chat)` — human-readable chat markup.
|
|
529
|
+
|
|
530
|
+
2. **On load** (`Persist.load_drivers[:chat]`):
|
|
531
|
+
- Reads the file content as a string and parses it with `LLM.chat(string)`.
|
|
532
|
+
- Returns a `Chat` instance.
|
|
533
|
+
|
|
534
|
+
3. **File location**: `<step.path>` (which already has no extension for chat
|
|
535
|
+
types, since the extension is `:chat` and `TYPE_EXTENSIONS[:chat] = :chat`).
|
|
536
|
+
The job result file IS the `.chat` file.
|
|
537
|
+
|
|
538
|
+
4. **Loading a dependency's chat**: `step(:dep_name).load` returns the parsed
|
|
539
|
+
Chat object, which can then be `follow`ed into the current chat.
|
|
540
|
+
|
|
541
|
+
### 9.3 The `Chat.project` function
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
def self.project(job, messages)
|
|
545
|
+
projected = Array(messages).reject { |message| message[:role].to_s == 'meta' }.collect(&:dup)
|
|
546
|
+
return [] if projected.empty?
|
|
547
|
+
[{ role: :meta, content: serialize_meta(job: job.to_s) }] + projected
|
|
548
|
+
end
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Purpose:** When a chat task completes, its result messages are "projected"
|
|
552
|
+
from the job. The projection:
|
|
553
|
+
1. Strips any existing `:meta` messages (to avoid accumulation).
|
|
554
|
+
2. Prepends a single `:meta` marker with `job=<short_path>`.
|
|
555
|
+
3. Returns the tagged message array.
|
|
556
|
+
|
|
557
|
+
This meta marker is how downstream tasks and the `Chat.jobs` method discover
|
|
558
|
+
which job produced a given chat segment.
|
|
559
|
+
|
|
560
|
+
### 9.4 The `Chat.jobs` method
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
def job_paths
|
|
564
|
+
role_messages(:meta).collect do |message|
|
|
565
|
+
Path.setup(Chat.parse_meta(message[:content])[:job])
|
|
566
|
+
end.compact.uniq
|
|
567
|
+
end
|
|
568
|
+
alias jobs job_paths
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Scans the chat for `:meta` role messages, parses each one to extract the `job=`
|
|
572
|
+
key, and returns the list of job paths. This is how the system builds the
|
|
573
|
+
provenance graph: a chat that follows another job's output inherits its meta
|
|
574
|
+
markers, and `chat.jobs` reveals all contributing jobs.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## 10. Patches to Existing Classes
|
|
579
|
+
|
|
580
|
+
### 10.1 `Workflow.require_workflow` — agent-aware fallback
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
class << self
|
|
584
|
+
alias require_workflow_old require_workflow
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def self.require_workflow(name, ...)
|
|
588
|
+
begin
|
|
589
|
+
require_workflow_old(name, ...)
|
|
590
|
+
rescue => e
|
|
591
|
+
begin
|
|
592
|
+
LLM.load_agent(name).workflow # ← if not a regular workflow, try loading as agent
|
|
593
|
+
rescue
|
|
594
|
+
raise e
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
When `Workflow.require_workflow("SomeAgent")` fails because no standard workflow
|
|
601
|
+
file exists, it falls back to loading the name as an agent and extracting its
|
|
602
|
+
`.workflow`. This is how agent-based workflows like `Planned` are resolved.
|
|
603
|
+
|
|
604
|
+
### 10.2 `LLM::Agent#job=` — provenance link
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
class LLM::Agent
|
|
608
|
+
attr_accessor :job
|
|
609
|
+
end
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
This adds a `job` accessor to every agent. The `agent` helper sets
|
|
613
|
+
`agent.job = self` (the current Step). This allows the agent's `ask` method to
|
|
614
|
+
detect when it is running inside a workflow and route inference through the
|
|
615
|
+
workflow's own `:ask` task (see §11.2).
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## 11. Job Context Propagation
|
|
620
|
+
|
|
621
|
+
### 11.1 How context flows into agents
|
|
622
|
+
|
|
623
|
+
When the `agent` helper is called inside a `chat_task` block, the agent receives:
|
|
624
|
+
|
|
625
|
+
1. **Tooling** — from `self.tooling` (extracted from the input chat)
|
|
626
|
+
2. **Options** — from `self.options` (extracted from `:option` messages in the chat)
|
|
627
|
+
3. **Conversation** — from the `chat:` parameter (typically `self.chat`)
|
|
628
|
+
4. **System context** — CWD, job path, files_dir, dependency paths
|
|
629
|
+
5. **Files** — from the `files:` parameter, resolved via `Step#file`
|
|
630
|
+
|
|
631
|
+
### 11.2 How agents route back through workflows
|
|
632
|
+
|
|
633
|
+
When an agent has a `workflow` with an `:ask` task AND has `job` set (meaning it
|
|
634
|
+
is running inside a `chat_task`), its `ask` method does NOT call `LLM.ask`
|
|
635
|
+
directly. Instead:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
# From agent.rb, simplified:
|
|
639
|
+
if workflow && workflow.tasks.include?(:ask) && !no_ask_override
|
|
640
|
+
job = workflow.job(:ask, chat: Chat.print(messages))
|
|
641
|
+
self.job = job
|
|
642
|
+
Chat.allow_dir job.files_dir
|
|
643
|
+
Chat.allow_read_dir workflow.directory
|
|
644
|
+
job.produce
|
|
645
|
+
messages = Chat.project(job.short_path, LLM.chat(job.path))
|
|
646
|
+
else
|
|
647
|
+
LLM.ask messages, ...
|
|
648
|
+
end
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
This creates a **nested workflow job** for each inference call, which:
|
|
652
|
+
- Is itself cached/persisted as a Scout job
|
|
653
|
+
- Has its own `files_dir` for agent-generated artifacts
|
|
654
|
+
- Creates a provenance chain (parent job → ask job)
|
|
655
|
+
- Can be inspected by `ChatAnalyst` and `scout llm prov`
|
|
656
|
+
|
|
657
|
+
The `no_ask_override` flag (set to `true` in the `Critic` workflow's agent
|
|
658
|
+
helper) bypasses this routing and calls `LLM.ask` directly.
|
|
659
|
+
|
|
660
|
+
### 11.3 Directory permissions
|
|
661
|
+
|
|
662
|
+
The agent's `ask` method calls:
|
|
663
|
+
```ruby
|
|
664
|
+
Chat.allow_dir job.files_dir # write access for tool calls
|
|
665
|
+
Chat.allow_read_dir workflow.directory # read access for workflow resources
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
These register thread-local allowed directories that the tool execution sandbox
|
|
669
|
+
respects. This is how agents get controlled filesystem access.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 12. The `chat_task` Lifecycle (End-to-End)
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
677
|
+
│ DECLARATION PHASE │
|
|
678
|
+
│ │
|
|
679
|
+
│ chat_task :my_task do │
|
|
680
|
+
│ # 'self' is the Step instance │
|
|
681
|
+
│ # self.chat → parsed Chat from input │
|
|
682
|
+
│ # self.options → LLM options from chat │
|
|
683
|
+
│ # self.tooling → extracted tool/kb/mcp/introduce messages │
|
|
684
|
+
│ agent = self.agent :MyAgent, chat: self.chat │
|
|
685
|
+
│ agent.user "Do something" │
|
|
686
|
+
│ agent # ← return value │
|
|
687
|
+
│ end │
|
|
688
|
+
└───────────────────────────────┬──────────────────────────────────────┘
|
|
689
|
+
│
|
|
690
|
+
▼
|
|
691
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
692
|
+
│ EXECUTION PHASE (chat_task body) │
|
|
693
|
+
│ │
|
|
694
|
+
│ 1. Input :chat is parsed → self.chat (memoized) │
|
|
695
|
+
│ 2. Block executed via instance_exec │
|
|
696
|
+
│ 3. self.agent(:MyAgent) called: │
|
|
697
|
+
│ a. LLM.load_agent loads agent from filesystem │
|
|
698
|
+
│ b. agent.job = self (current Step) │
|
|
699
|
+
│ c. agent.start_chat populated with: │
|
|
700
|
+
│ - tooling messages (tools, KB, MCP, introductions) │
|
|
701
|
+
│ - system messages (CWD, job path, files_dir, deps) │
|
|
702
|
+
│ - conversation context (filtered chat) │
|
|
703
|
+
│ - file attachments │
|
|
704
|
+
│ 4. agent.user("Do something") adds a user message │
|
|
705
|
+
│ 5. Block returns the agent │
|
|
706
|
+
└───────────────────────────────┬──────────────────────────────────────┘
|
|
707
|
+
│
|
|
708
|
+
▼
|
|
709
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
710
|
+
│ RESULT PROCESSING (chat_task wrapper) │
|
|
711
|
+
│ │
|
|
712
|
+
│ 6. response = block return value (an LLM::Agent) │
|
|
713
|
+
│ 7. Since last message is :user → agent.chat(return_messages: true) │
|
|
714
|
+
│ a. Agent#ask detects workflow.tasks.include?(:ask) │
|
|
715
|
+
│ b. Creates nested workflow job: workflow.job(:ask, chat: ...) │
|
|
716
|
+
│ c. job.produce → runs inference, persists result │
|
|
717
|
+
│ d. Returns projected messages │
|
|
718
|
+
│ 8. log_agent(agent) called automatically: │
|
|
719
|
+
│ a. Writes log/agent.chat (full conversation) │
|
|
720
|
+
│ b. Writes log/society/*.chat for delegated agents │
|
|
721
|
+
│ c. Updates step info with dependency paths │
|
|
722
|
+
│ 9. Chat.project(self.short_path, result) wraps with provenance meta │
|
|
723
|
+
│ 10. Result saved as <step.path> with .chat extension │
|
|
724
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## 13. Real Usage Examples from SC26
|
|
730
|
+
|
|
731
|
+
### 13.1 Planned — Linear Pipeline (`Agent/Planned/workflow.rb`)
|
|
732
|
+
|
|
733
|
+
The canonical multi-step agent pipeline. Each `chat_task` depends on the
|
|
734
|
+
previous one, building up context through `chat.follow`:
|
|
735
|
+
|
|
736
|
+
```ruby
|
|
737
|
+
module Planned
|
|
738
|
+
extend Workflow
|
|
739
|
+
self.include_workflow AgentWorkflow
|
|
740
|
+
|
|
741
|
+
chat_task :request do
|
|
742
|
+
agent = self.agent :User, chat: chat
|
|
743
|
+
agent.user "Restate the request from the user..."
|
|
744
|
+
agent
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
dep :request
|
|
748
|
+
chat_task :search do
|
|
749
|
+
chat = self.chat
|
|
750
|
+
chat.follow step(:request).load # ← append request's chat output
|
|
751
|
+
agent = self.agent :Searcher, chat: chat, tooling: self.tooling_intro
|
|
752
|
+
agent.user "Prepare a research report..."
|
|
753
|
+
agent
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
dep :search # ← conditional dependency
|
|
757
|
+
chat_task :plan do
|
|
758
|
+
chat = self.chat
|
|
759
|
+
chat.follow step(:request).load.last
|
|
760
|
+
chat.follow step(:search).load.last if step(:search)
|
|
761
|
+
chat.message :clear_tools, true # ← strip tools from context
|
|
762
|
+
agent = self.agent :Planner, chat: chat, tooling: self.tooling_intro
|
|
763
|
+
agent.user "Elaborate a plan..."
|
|
764
|
+
agent
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
dep :plan
|
|
768
|
+
chat_task :work do
|
|
769
|
+
chat = self.chat
|
|
770
|
+
chat.follow step(:request).load.last
|
|
771
|
+
chat.follow step(:plan).load.last
|
|
772
|
+
chat.message :clear_tools, true
|
|
773
|
+
agent = self.agent worker_agent, chat: chat, tooling: self.tooling
|
|
774
|
+
agent.user "Proceed with the plan..."
|
|
775
|
+
agent
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
dep :work
|
|
779
|
+
chat_task :ask do
|
|
780
|
+
chat = self.chat
|
|
781
|
+
chat.follow step(:request).load
|
|
782
|
+
chat.follow step(:plan).load
|
|
783
|
+
chat.follow step(:work).load
|
|
784
|
+
chat.message :clear_tools, true
|
|
785
|
+
agent = self.agent :User, tooling: false, chat: chat # ← no tools
|
|
786
|
+
agent.user "Please elaborate a final report..."
|
|
787
|
+
agent
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Pattern: Linear dependency chain with context accumulation.** Each step:
|
|
793
|
+
1. Declares `dep :previous_step`
|
|
794
|
+
2. Follows previous step's output into the chat
|
|
795
|
+
3. May clear tools to focus the agent
|
|
796
|
+
4. May use different tooling levels (`tooling_intro` vs `tooling` vs `false`)
|
|
797
|
+
|
|
798
|
+
### 13.2 Critic — Direct Inference (`Agent/Critic/workflow.rb`)
|
|
799
|
+
|
|
800
|
+
Uses `no_ask_override: true` to bypass workflow routing:
|
|
801
|
+
|
|
802
|
+
```ruby
|
|
803
|
+
module Critic
|
|
804
|
+
extend Workflow
|
|
805
|
+
self.include_workflow AgentWorkflow
|
|
806
|
+
|
|
807
|
+
chat_task :ask do
|
|
808
|
+
agent = self.agent :Critic, chat: chat, no_ask_override: true
|
|
809
|
+
agent.user "Evaluate the previous work."
|
|
810
|
+
response = agent.chat return_messages: true
|
|
811
|
+
begin
|
|
812
|
+
set_info :json, Chat.parse_json(response.answer)
|
|
813
|
+
rescue
|
|
814
|
+
end
|
|
815
|
+
agent
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Pattern: Structured output with side-effect metadata.** The `set_info` call
|
|
821
|
+
writes parsed JSON to the step's info file, making it queryable via Scout's
|
|
822
|
+
metadata system.
|
|
823
|
+
|
|
824
|
+
### 13.3 Branched — Parallel Fan-out (`Agent/Branched/workflow.rb`)
|
|
825
|
+
|
|
826
|
+
Uses `log_agent` manually for multiple agents, and `iterate_dictionary` for
|
|
827
|
+
parallel execution:
|
|
828
|
+
|
|
829
|
+
```ruby
|
|
830
|
+
chat_task :work do
|
|
831
|
+
# ... spliter agent produces a JSON dict of sub-tasks ...
|
|
832
|
+
|
|
833
|
+
reports = {}
|
|
834
|
+
spliter.iterate_dictionary nil, cpus: 8, bar: self.progress_bar('Branches'),
|
|
835
|
+
into: reports do |name, instructions|
|
|
836
|
+
log name, "Start #{name}"
|
|
837
|
+
worker = self.agent worker_agent, chat: plan.dup, tooling: tooling
|
|
838
|
+
worker.user "Produce sub-task (#{name}):\n\n#{instructions}"
|
|
839
|
+
reports[name] = worker.chat
|
|
840
|
+
log_agent worker, "worker-#{name}" # ← manual logging with named subdir
|
|
841
|
+
log name, "Done #{name}"
|
|
842
|
+
[name, worker.chat]
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
log_agent spliter, 'spliter' # ← log the spliter too
|
|
846
|
+
|
|
847
|
+
# ... critic aggregates reports ...
|
|
848
|
+
end
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**Pattern: Manual agent management with parallel execution.** Key techniques:
|
|
852
|
+
- `iterate_dictionary` for JSON-dict-driven parallel fan-out
|
|
853
|
+
- `chat: plan.dup` to give each branch an independent copy of the plan
|
|
854
|
+
- Manual `log_agent` calls with distinct names for each branch
|
|
855
|
+
- `self.progress_bar` for visual feedback
|
|
856
|
+
|
|
857
|
+
### 13.4 Refined — Iterative Loop (`Agent/Refined/workflow.rb`)
|
|
858
|
+
|
|
859
|
+
Uses `TryAgain` exception for control flow:
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
chat_task :ask do
|
|
863
|
+
worker = self.agent worker_agent, chat: chat, tooling: self.tooling
|
|
864
|
+
critic = self.agent :Critic, chat: chat
|
|
865
|
+
|
|
866
|
+
round = 1
|
|
867
|
+
begin
|
|
868
|
+
worker.user "Execute the work..."
|
|
869
|
+
report = worker.chat
|
|
870
|
+
|
|
871
|
+
critic.user "Below is the workers report"
|
|
872
|
+
critic.user report
|
|
873
|
+
critic.user "Make an evaluation in JSON."
|
|
874
|
+
json = critic.json
|
|
875
|
+
evaluation = IndiferentHash.setup(json)
|
|
876
|
+
|
|
877
|
+
log_agent worker, "worker-round-#{round}"
|
|
878
|
+
log_agent critic, "critic-round-#{round}"
|
|
879
|
+
|
|
880
|
+
case evaluation[:status]
|
|
881
|
+
when 'NEEDS_WORK'
|
|
882
|
+
round += 1
|
|
883
|
+
worker.user "The Critic has done this evaluation..."
|
|
884
|
+
worker.user evaluation.to_json
|
|
885
|
+
worker.message :clear_tools, true
|
|
886
|
+
critic.message :clear_tools, true
|
|
887
|
+
raise TryAgain
|
|
888
|
+
when 'PASS'
|
|
889
|
+
when 'BLOCKED'
|
|
890
|
+
end
|
|
891
|
+
rescue
|
|
892
|
+
retry if TryAgain === $!
|
|
893
|
+
raise $!
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
{role: :assistant, content: evaluation.to_json}
|
|
897
|
+
end
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**Pattern: Worker-critic loop with exception-driven retry.** Key techniques:
|
|
901
|
+
- `raise TryAgain` + `retry if TryAgain === $!` for loop control
|
|
902
|
+
- Round-specific `log_agent` names for provenance
|
|
903
|
+
- `:clear_tools` between rounds to manage context
|
|
904
|
+
- Returns a Hash (not an Agent) — the chat_task wrapper handles it
|
|
905
|
+
|
|
906
|
+
### 13.5 InterpretData — Dynamic Tool Definition (`Agent/InterpretData/workflow.rb`)
|
|
907
|
+
|
|
908
|
+
Uses `agent.workflow do ... end` to define tasks inline:
|
|
909
|
+
|
|
910
|
+
```ruby
|
|
911
|
+
chat_task :gather do
|
|
912
|
+
analyst = self.agent :Analyst, chat: chat, tooling: self.tooling
|
|
913
|
+
|
|
914
|
+
artifact_dir = file('artifacts')
|
|
915
|
+
|
|
916
|
+
analyst.workflow do
|
|
917
|
+
desc "Write an artifact to file"
|
|
918
|
+
input :name, :string, 'Name of the artifact', nil, required: true
|
|
919
|
+
input :content, :text, 'Content of the artifact', nil, required: true
|
|
920
|
+
task :write_artifact => :string do |name, content|
|
|
921
|
+
artifact_dir[name].write content
|
|
922
|
+
end
|
|
923
|
+
export_exec :write_artifact
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
analyst.user "Analyze the data and save artifacts..."
|
|
927
|
+
analyst
|
|
928
|
+
end
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
**Pattern: Runtime tool injection.** The `agent.workflow do ... end` block
|
|
932
|
+
calls `LLM::Agent#workflow(&block)` which does `workflow.instance_eval(&block)`,
|
|
933
|
+
adding tasks to the agent's workflow module. These tasks become callable tools
|
|
934
|
+
for the agent. The `export_exec` makes them available as executable tools.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## 14. How Tooling Flows Through Workflows to Agents
|
|
939
|
+
|
|
940
|
+
```
|
|
941
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
942
|
+
│ Input Chat (from user or upstream task) │
|
|
943
|
+
│ │
|
|
944
|
+
│ Messages with roles: │
|
|
945
|
+
│ :introduce → "You are the Planner agent..." │
|
|
946
|
+
│ :tool → {"type":"function","function":{...}} │
|
|
947
|
+
│ :kb → knowledge base definitions │
|
|
948
|
+
│ :mcp → MCP server definitions │
|
|
949
|
+
│ :option → "model gpt-4o" │
|
|
950
|
+
│ :user → "Please analyze the data..." │
|
|
951
|
+
│ :assistant → (prior responses) │
|
|
952
|
+
│ :meta → "job=Planned/request/abc123" │
|
|
953
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
954
|
+
│
|
|
955
|
+
▼
|
|
956
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
957
|
+
│ chat_task body │
|
|
958
|
+
│ │
|
|
959
|
+
│ self.tooling ──→ removes :tool, :kb, :mcp, :introduce from chat │
|
|
960
|
+
│ returns them as a combined array │
|
|
961
|
+
│ (memoized: extracted once) │
|
|
962
|
+
│ │
|
|
963
|
+
│ self.tooling_intro ──→ subset of tooling: only :introduce msgs │
|
|
964
|
+
│ │
|
|
965
|
+
│ self.options ──→ LLM.options(self.chat) extracts :option msgs │
|
|
966
|
+
│ │
|
|
967
|
+
│ self.agent(:MyAgent, │
|
|
968
|
+
│ chat: self.chat, ← conversation (minus tooling) │
|
|
969
|
+
│ tooling: self.tooling, ← full tooling (default) │
|
|
970
|
+
│ options: self.options ← LLM config (default) │
|
|
971
|
+
│ ) │
|
|
972
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
973
|
+
│
|
|
974
|
+
▼
|
|
975
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
976
|
+
│ agent.start_chat (after agent helper) │
|
|
977
|
+
│ │
|
|
978
|
+
│ [tooling messages] ← :tool, :kb, :mcp, :introduce │
|
|
979
|
+
│ [system messages] ← CWD, job path, deps │
|
|
980
|
+
│ [conversation messages] ← filtered chat context │
|
|
981
|
+
│ [file attachments] ← if files: param given │
|
|
982
|
+
│ │
|
|
983
|
+
│ Then agent.user("prompt") adds the task prompt │
|
|
984
|
+
│ Then agent.chat() triggers inference │
|
|
985
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
**Key insight:** Tooling is **extracted** from the input chat (mutating it)
|
|
989
|
+
and **selectively re-injected** into each agent. This allows different agents
|
|
990
|
+
in the same pipeline to have different tool sets:
|
|
991
|
+
|
|
992
|
+
| Agent type | `tooling:` param | What the agent gets |
|
|
993
|
+
|---|---|---|
|
|
994
|
+
| Planner | `self.tooling_intro` | Only introductions (understands tools conceptually, can't call them) |
|
|
995
|
+
| Worker | `self.tooling` | Full tooling (can call all tools) |
|
|
996
|
+
| User (final report) | `false` | No tools at all (pure text synthesis) |
|
|
997
|
+
| Critic | (not specified → `self.tooling`) | Full tooling by default |
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## 15. Design Patterns and When to Use AgentWorkflow
|
|
1002
|
+
|
|
1003
|
+
### 15.1 When to use `chat_task` vs regular `task`
|
|
1004
|
+
|
|
1005
|
+
**Use `chat_task` when:**
|
|
1006
|
+
- The task's primary input is a Chat (conversation context)
|
|
1007
|
+
- The task's output should be a `.chat` file
|
|
1008
|
+
- The task involves agent interaction (loading an agent, sending prompts)
|
|
1009
|
+
- You want automatic provenance tracking via `Chat.project`
|
|
1010
|
+
- You want automatic agent logging via `log_agent`
|
|
1011
|
+
|
|
1012
|
+
**Use regular `task` when:**
|
|
1013
|
+
- The input/output types are different (TSV, JSON, etc.)
|
|
1014
|
+
- The task is pure data transformation with no agent involvement
|
|
1015
|
+
- You need fine-grained control over persistence
|
|
1016
|
+
|
|
1017
|
+
### 15.2 When to use `include_workflow AgentWorkflow`
|
|
1018
|
+
|
|
1019
|
+
**Always include it when:**
|
|
1020
|
+
- You define `chat_task`s (the task type needs the helpers)
|
|
1021
|
+
- You use `self.agent`, `self.chat`, `self.tooling`, etc.
|
|
1022
|
+
|
|
1023
|
+
**You don't need it when:**
|
|
1024
|
+
- Your workflow has only regular tasks (no agent interaction)
|
|
1025
|
+
- You're using agent delegation purely at the agent level (not through workflows)
|
|
1026
|
+
|
|
1027
|
+
### 15.3 Provenance patterns
|
|
1028
|
+
|
|
1029
|
+
The AgentWorkflow creates a multi-layered provenance trail:
|
|
1030
|
+
|
|
1031
|
+
```
|
|
1032
|
+
User Chat
|
|
1033
|
+
└─ Planned/ask/<id>.chat ← final report task
|
|
1034
|
+
└─ Planned/work/<id>.chat ← work task (dep of ask)
|
|
1035
|
+
└─ Worker/ask/<id>.chat ← individual inference calls
|
|
1036
|
+
└─ (LLM.ask cache) ← raw LLM API call cache
|
|
1037
|
+
└─ Planned/plan/<id>.chat ← plan task (dep of ask)
|
|
1038
|
+
└─ Planned/request/<id>.chat ← request task (dep of ask)
|
|
1039
|
+
└─ log/agent.chat ← full agent conversations
|
|
1040
|
+
└─ log/society/*.chat ← delegated agent chats
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
Each level is independently cached, resumable, and queryable by `ChatAnalyst`
|
|
1044
|
+
and `scout llm prov`.
|
|
1045
|
+
|
|
1046
|
+
### 15.4 Common idioms
|
|
1047
|
+
|
|
1048
|
+
| Idiom | Code | Purpose |
|
|
1049
|
+
|---|---|---|
|
|
1050
|
+
| Follow dependency output | `chat.follow step(:dep).load` | Append a dependency's chat result |
|
|
1051
|
+
| Follow only the last message | `chat.follow step(:dep).load.last` | Get just the final answer |
|
|
1052
|
+
| Clear tools for focus | `chat.message :clear_tools, true` | Remove tool definitions from context |
|
|
1053
|
+
| Configurable worker agent | `options[:worker_agent] \|\| 'Worker'` | Allow runtime agent selection |
|
|
1054
|
+
| Conditional dependency | `dep :search do \|jobname,options\| ... end` | Skip search if `use_search` is false |
|
|
1055
|
+
| Named agent logging | `log_agent agent, "worker-#{name}"` | Create subdirectories for multiple agents |
|
|
1056
|
+
| Structured metadata | `set_info :json, parsed_json` | Store parsed output in step info |
|
|
1057
|
+
| Exception-driven retry | `raise TryAgain; retry if TryAgain === $!` | Loop control in iterative patterns |
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
## 16. Method Reference Summary
|
|
1062
|
+
|
|
1063
|
+
| Method | Defined in | Signature | Returns | Auto-called? |
|
|
1064
|
+
|---|---|---|---|---|
|
|
1065
|
+
| `chat_task` | `Workflow` (patched) | `(name, &block)` | — | No (DSL) |
|
|
1066
|
+
| `chat` | `AgentWorkflow` helper | `(chat=nil)` | `Chat` | No |
|
|
1067
|
+
| `options` | `AgentWorkflow` helper | `()` | `Hash` | No |
|
|
1068
|
+
| `agent` | `AgentWorkflow` helper | `(name=nil, chat:nil, options:nil, tooling:nil, files:nil, **kwargs)` | `LLM::Agent` | No |
|
|
1069
|
+
| `agent_options` | `AgentWorkflow` helper | `(options)` | `Hash` | No (internal) |
|
|
1070
|
+
| `tooling` | `AgentWorkflow` helper | `()` | `Array<Hash>` | No |
|
|
1071
|
+
| `tooling_intro` | `AgentWorkflow` helper | `()` | `Array<Hash>` | No |
|
|
1072
|
+
| `log_agent` | `AgentWorkflow` helper | `(agent, agent_name=nil)` | `LLM::Agent` | Yes (by `chat_task` when block returns Agent) |
|
|
1073
|
+
| `add_chat_dependencies` | `AgentWorkflow` helper | `(chat)` | void | No (commented out) |
|
|
1074
|
+
| `Chat.project` | `Chat` class method | `(job, messages)` | `Array<Hash>` | Yes (by `chat_task`) |
|
|
1075
|
+
| `Chat.jobs` / `job_paths` | `Chat` instance | `()` | `Array<Path>` | No |
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
## 17. Relationship to the Broader Agent Ecosystem
|
|
1080
|
+
|
|
1081
|
+
```
|
|
1082
|
+
┌─────────────────────┐
|
|
1083
|
+
│ LLM::Agent │
|
|
1084
|
+
│ (stateful wrapper) │
|
|
1085
|
+
└────────┬────────────┘
|
|
1086
|
+
│
|
|
1087
|
+
┌───────────────────┼───────────────────┐
|
|
1088
|
+
│ │ │
|
|
1089
|
+
┌─────────▼──────┐ ┌────────▼────────┐ ┌───────▼────────┐
|
|
1090
|
+
│ agent/chat.rb │ │ agent/delegate │ │ agent/workflow │
|
|
1091
|
+
│ (conversation │ │ (multi-agent │ │ (AgentWorkflow │
|
|
1092
|
+
│ lifecycle) │ │ delegation) │ │ + chat_task) │
|
|
1093
|
+
└────────────────┘ └─────────────────┘ └───────┬────────┘
|
|
1094
|
+
│
|
|
1095
|
+
┌──────────▼──────────┐
|
|
1096
|
+
│ Scout Workflow │
|
|
1097
|
+
│ (persistence, │
|
|
1098
|
+
│ dependencies, │
|
|
1099
|
+
│ provenance, │
|
|
1100
|
+
│ scheduling) │
|
|
1101
|
+
└─────────────────────┘
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
`AgentWorkflow` is the **integration layer** that connects the Agent abstraction
|
|
1105
|
+
to Scout's Workflow system. It provides:
|
|
1106
|
+
- **Task abstraction:** `chat_task` makes agent conversations first-class
|
|
1107
|
+
workflow steps
|
|
1108
|
+
- **Persistence:** Results are automatically saved as `.chat` files with
|
|
1109
|
+
proper serialization
|
|
1110
|
+
- **Provenance:** `Chat.project` + `log_agent` create a complete evidence trail
|
|
1111
|
+
- **Composability:** Dependencies between chat tasks enable complex pipelines
|
|
1112
|
+
- **Configurability:** Tooling and options flow through the chat input,
|
|
1113
|
+
allowing runtime configuration
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
*This document covers `lib/scout/llm/agent/workflow.rb` in full and cross-references
|
|
1118
|
+
all related source files. For the broader agent class and delegation mechanics,
|
|
1119
|
+
see `03_agent_and_delegation.md`. For multi-agent orchestration patterns, see
|
|
1120
|
+
`08_multi_agent_patterns.md`.*
|