scout-ai 2.0.0 → 2.1.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 +5 -0
- data/VERSION +1 -1
- data/doc/developer/ChatLifecycle.md +1 -1
- data/doc/developer/DelegationInternals.md +1 -1
- data/doc/developer/Provenance.md +15 -5
- data/lib/scout/llm/agent/attach.rb +68 -0
- data/lib/scout/llm/agent/workflow.rb +2 -1
- data/lib/scout/llm/agent.rb +9 -1
- data/lib/scout/llm/ask.rb +11 -12
- data/lib/scout/llm/backends/default.rb +9 -3
- data/lib/scout/llm/chat/process/tools.rb +2 -5
- data/lib/scout/llm/chat/provenance.rb +61 -2
- data/research/provenance-continuation-accounting.md +255 -0
- data/scout-ai.gemspec +4 -2
- data/scout_commands/llm/prov +121 -21
- data/test/scout/llm/chat/agent_meta_fixtures.rb +91 -5
- data/test/scout/llm/chat/test_agent_meta_provenance.rb +133 -0
- data/test/scout/llm/chat/test_agent_meta_tokens.rb +54 -0
- data/test/scout/llm/chat/test_prov_cli.rb +406 -6
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4c45ad3bd7a6219e9fe1e4e0611391f5751f7e3faa253f8db9b132ecc2e6bae
|
|
4
|
+
data.tar.gz: 7068ae89ef507efa50eadf36d9b563b7a596fda8b31bd29cd3f3f6c21c40aba3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb97d4db5f611da3302ccbecc5d1e567d50fe1fe96916150fb5b4445d5e74f887c8f68e35e1b0111667705491a0359802304ee99090b05cbde4401834c538657
|
|
7
|
+
data.tar.gz: 6877551eda1ebf3d916bc2b1f14347d3323410ee832e62838b11f137c3b5cdc86aaff4d41c01c546997312a387e7ec2d333bc85dde2fa586ec72201fc34221c9
|
data/.vimproject
CHANGED
|
@@ -62,6 +62,10 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
save_locations
|
|
65
|
+
provenance_cortex
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
test_abort
|
|
65
69
|
}
|
|
66
70
|
lib=lib {
|
|
67
71
|
scout-ai.rb
|
|
@@ -124,6 +128,7 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
124
128
|
agent=agent{
|
|
125
129
|
chat.rb
|
|
126
130
|
delegate.rb
|
|
131
|
+
attach.rb
|
|
127
132
|
iterate.rb
|
|
128
133
|
save.rb
|
|
129
134
|
workflow.rb
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.1.0
|
|
@@ -48,7 +48,7 @@ recognizes these roles:
|
|
|
48
48
|
| `user` | User message | Yes |
|
|
49
49
|
| `assistant` | Model response | Yes |
|
|
50
50
|
| `function_call` | Tool invocation request from model | Yes (as provider-specific tool_call) |
|
|
51
|
-
| `function_call_output` | Tool execution result. May carry a `meta` key: an Array of already-deserialized receipt field Hashes (delegated inference metadata such as `pt`/`ct`/`tt`/`inference_id`, or `job
|
|
51
|
+
| `function_call_output` | Tool execution result. May carry a `meta` key: an Array of already-deserialized receipt field Hashes (delegated inference metadata such as `pt`/`ct`/`tt`/`inference_id`, or `{"job":"<path>"}` producer references), embedded by `LLM.process_calls` when the tool returned an `LLM::Agent`. Legacy chats may instead carry a serialized `agent_meta` key; both are read, `meta` wins when both are present. Outputs may also carry auxiliary `step`/`start_timestamp`/`timestamp` fields; `step` is bookkeeping, not a provenance edge. See [Provenance.md](Provenance.md). | Yes (as provider-specific tool result) |
|
|
52
52
|
| `meta` | Provenance metadata (tokens, job references) | **No** — stripped before inference |
|
|
53
53
|
| `tool` | Tool definition (inline in chat) | No — extracted into tool registry |
|
|
54
54
|
| `introduce` | Workflow/tool introduction | No — extracted, introduces tools to the model context |
|
|
@@ -223,7 +223,7 @@ When a delegation tool returns an `LLM::Agent`, `LLM.process_calls` embeds
|
|
|
223
223
|
the child agent's `meta` messages in the parent `function_call_output`
|
|
224
224
|
envelope under the `meta` key, as an Array of **already-deserialized** field
|
|
225
225
|
Hashes (one for the child's own inference metadata, one per producer
|
|
226
|
-
reference). These receipts are provenance evidence, not parent-chat
|
|
226
|
+
reference: a `job` key holding the child job path). These receipts are provenance evidence, not parent-chat
|
|
227
227
|
messages: the child's inference metadata and producer job reference are read
|
|
228
228
|
from the paired tool output and never injected into the parent chat. Provenance
|
|
229
229
|
tooling consumes them through `Chat.agent_meta_evidence` and the `:agent_job`
|
data/doc/developer/Provenance.md
CHANGED
|
@@ -18,7 +18,7 @@ The structural relations are:
|
|
|
18
18
|
| Parent | Relation | Child | Meaning |
|
|
19
19
|
|---|---|---|---|
|
|
20
20
|
| chat | `job` | job | A projected response was produced by a Workflow job. |
|
|
21
|
-
| chat | `agent_job` | job | A delegated tool call returned an agent whose `job
|
|
21
|
+
| chat | `agent_job` | job | A delegated tool call returned an agent whose receipt entry carries a `job` field naming the producer job. |
|
|
22
22
|
| job | `dependency` | job | A normal Scout Workflow dependency. |
|
|
23
23
|
| job | `log` | chat | A persisted agent conversation under `.files/*.chat`, `.files/*.society/**/*.chat`, or the legacy `.files/log/**/*.chat`. |
|
|
24
24
|
| chat | `log` | chat | A saved agent conversation under the chat's own `.files` sidecar, same three families (root copy excluded). |
|
|
@@ -195,10 +195,12 @@ Two receipt formats exist, and the reader accepts both:
|
|
|
195
195
|
- **Current format — the `meta` key.** The writer deserializes the child agent's `meta` messages (`LLM.meta_receipt_from_messages`) and emits an Array of plain field Hashes, each already parsed:
|
|
196
196
|
|
|
197
197
|
```
|
|
198
|
-
function_call_output: {"name":"ask","content":"child answer","id":"call_1","meta":[{"pt":100,"ct":50,"tt":150,"inference_id":"aaa"},{"job":"
|
|
198
|
+
function_call_output: {"name":"ask","content":"child answer","id":"call_1","meta":[{"pt":100,"ct":50,"tt":150,"inference_id":"aaa"},{"job":"Cortex/continue/Default_x.chat"}]}
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
An entry carries either the child's direct inference metadata (`pt`, `ct`, `tt`, ..., `inference_id`) or a producer reference
|
|
201
|
+
An entry carries either the child's direct inference metadata (`pt`, `ct`, `tt`, ..., `inference_id`) or a producer reference: a `job` key holding the child job path, e.g. `{"job":"Cortex/continue/Default_x.chat"}`. Job references may be job-typed or chat-typed paths; both resolve to a Step. Entries that would carry no fields are dropped by the writer.
|
|
202
|
+
|
|
203
|
+
The same `function_call_output` may also carry auxiliary fields next to the receipt: `step` (the producing step of the same execution), `start_timestamp`, and `timestamp`. They are bookkeeping only: the sole receipt-driven provenance edge source is the `job` field of a receipt entry (`meta[].job`); the `step` field is never followed as a parent-child edge.
|
|
202
204
|
|
|
203
205
|
- **Legacy format — the `agent_meta` key.** Older data stores serialized meta messages:
|
|
204
206
|
|
|
@@ -235,7 +237,7 @@ Malformed warnings mirror the persisted key in their `evidence_address`, and alw
|
|
|
235
237
|
|
|
236
238
|
### The `agent_job` relation
|
|
237
239
|
|
|
238
|
-
`Chat::PROVENANCE_RELATIONS` includes `agent_job`: chat to delegated producer job, resolved from `job
|
|
240
|
+
`Chat::PROVENANCE_RELATIONS` includes `agent_job`: chat to delegated producer job, resolved from the `job` field of receipt entries (`meta[].job`; the auxiliary `step` field on the tool output is not an edge source). The child is a normal Step and follows `dependency`, `log`, and `result` as usual. A job reference whose Step path and `.info` sidecar both do not exist is not followed and is reported instead.
|
|
239
241
|
|
|
240
242
|
Diagnostics go through `Chat.provenance_error` with relation `:agent_job`; the error itself is a plain `ScoutException` whose message is built by `Chat.agent_meta_error_message`, and every structured fact (enclosing chat path, tool output address, receipt address, call id, tool name, malformed entry, reference, reason) travels in the `on_error` reference Hash. In strict mode (no `on_error`) a malformed receipt raises; with `on_error` each problem is reported once per receipt, while the rest of the chat's provenance still expands. Only output JSON that parses to a Hash carrying an explicit receipt key (current `meta`, legacy `agent_meta`) is ever inspected: unparseable tool outputs are never scanned for the substring `agent_meta`.
|
|
241
243
|
|
|
@@ -253,6 +255,8 @@ A missing `provider_response_id` in one evidence record and a present one in ano
|
|
|
253
255
|
|
|
254
256
|
`Chat.provenance_token_totals(root, scope:)` sums event tokens by scope. Scopes are **evidence coverage**, not a partition of cost: an event stored both in a saved child log and in a receipt belongs to `:chat_evidence` and to `:receipt_evidence`, so those two must never be summed together. `:deduplicated_total` (default) counts every event once and `:receipt_only` is the disjoint delegated contribution with no saved-chat evidence. `Chat.tokens(root)` delegates to the collector, so provenance aggregates include receipt-only child usage without double counting when the same child inference is also persisted in a job.
|
|
255
257
|
|
|
258
|
+
Detailed usage fields come from `Chat.normalize_usage` through `USAGE_FIELD_MAP`, which currently recognizes the OpenAI/Glm/Anthropic spellings present in the map. The map is the inclusivity boundary: a provider that reports cache or reasoning numbers under a spelling the map does not list yields an event whose `cct`/`cwt`/`rt` stay nil, so the renderer omits `cache=`-axis detail for it while `pt`/`ct`/`tt` still count. The short keys are the prov vocabulary (`pt`/`ct`/`tt`/`cct`/`cwt`/`rt`); provider field names never appear in prov output.
|
|
259
|
+
|
|
256
260
|
## Workflow failures and partial provenance
|
|
257
261
|
|
|
258
262
|
Traversal visits jobs regardless of `done?`. Error and aborted Steps may still have dependencies, Step info, results, or partial agent logs. Status and exception details remain authoritative in Step info.
|
|
@@ -279,7 +283,13 @@ Root classification uses the `.info` sidecar only: a path is a job iff `<path>.i
|
|
|
279
283
|
|
|
280
284
|
The default tree is a spanning-tree presentation of a DAG. Repeated nodes are displayed as seen references rather than recursively expanded. Compact and graphical flows choose natural data-flow arrow direction during rendering without changing traversal semantics.
|
|
281
285
|
|
|
282
|
-
|
|
286
|
+
Default-tree numbers are labelled `evidence=`: each node carries the subtree-deduplicated evidence closure of everything reachable from it, so sibling and ancestor lines overlap and the values must never be read as per-part cost (continuation carriers make closures cumulative as well). Job nodes add `delta=`: the direct token totals of the job's persisted chat-typed result (`Chat.job_result_chat_file`), which is exactly the receipts-defined accounting delta for that delegated part; jobs whose result is not a saved chat omit the field; `delta=` is printed in both modes, because in `--component` the `direct=`/`delta=` contrast on one line is the very question the mode answers. Per-job `delta=` values sum to the root total on continuation chains but not on general DAGs (a job whose result is re-sent to several parents is one logical delta, not several).
|
|
287
|
+
|
|
288
|
+
Both modes end with a `root deduplicated_total=` footer: `root deduplicated_total=<tt> (<N> events) prompt=... cache=<abs>@<rate>% fresh=... [cache_write=...] cont=... reason=... (authoritative cost; per-node evidence=/direct= values overlap)`, reusing the already-computed root closure, followed by the identity-conflict caveat line whenever conflicts make it non-authoritative (that footer caveat is the single home of the conflict warning; the `--component` scope block does not repeat it). The event count lives in the footer, not on the root node line. `--component` relabels the per-node numbers `direct=` (own direct logs, still not per-part cost on continuation carriers).
|
|
289
|
+
|
|
290
|
+
The prompt axis is contiguous and uses one canonical field order in node lines and the footer: qualifier total, `[delta=]`, `prompt=`, `cache=<abs>@<rate>`, `[fresh=]`, `[cache_write=]`, `cont=`, `reason=`. `cache=` is the cache-hit share of prompt (`cct`, summed over the events behind the figure) printed as `cache=<absolute>@<rate>%`, with the rate computed as `100.0 * cct / pt` from the raw integer totals and formatted `%.1f`; it prints whenever `pt > 0`, including `cache=0@0.0%` (a run with no provider cache data at all). When `pt` is nil or 0 the whole prompt axis is omitted. Node lines carry the compact form; the footer additionally carries `fresh=` (`pt - cct` raw, the prompt tokens not served from cache, which includes the cache-write portion) and `cache_write=` (`cwt`, Anthropic cache-write tokens; printed only when positive). Because conflicting evidence sums can yield `cct > pt`, the raw ratio may exceed 100%; the renderer prints whatever the raw ratio gives instead of clamping, and the conflict caveat line is the guard that marks such a figure best-effort.
|
|
291
|
+
|
|
292
|
+
The bare unqualified `total=` printed inside `--component` scope lines and `--evidence` rows is retained deliberately as coverage-line vocabulary: it is never summed and is not a cost figure.
|
|
283
293
|
|
|
284
294
|
Delegated calls are reported from receipts (current `meta` key or legacy `agent_meta` key; both formats are accepted transparently). The tree labels a job reached through a receipt as `delegated-job`, adds one `delegated receipt: N events, total=<tt>, <tools>` annotation line under chats that carry receipts, and `--component` prints `scope local:` / `scope receipt:` / `scope aggregate:` lines when receipt evidence exists. Flow and DOT render receipt edges as `delegated_result`.
|
|
285
295
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module LLM
|
|
2
|
+
class Agent
|
|
3
|
+
|
|
4
|
+
ATACH_TYPES = %w(auto image pdf png jpeg)
|
|
5
|
+
def attachments
|
|
6
|
+
@other_options[:tools] ||= {}
|
|
7
|
+
|
|
8
|
+
task_name = :attach
|
|
9
|
+
block = Proc.new do |_name, parameters|
|
|
10
|
+
begin
|
|
11
|
+
path = Path.setup(parameters[:file]).find
|
|
12
|
+
raise ScoutException, "Path not found: #{path}" unless path.exists?
|
|
13
|
+
|
|
14
|
+
file_type = (parameters[:file_type] || 'image').to_s
|
|
15
|
+
if file_type == 'auto' && path.get_extension.downcase == 'pdf'
|
|
16
|
+
file_type = 'pdf'
|
|
17
|
+
else
|
|
18
|
+
file_type = 'image'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
case
|
|
22
|
+
when 'image', 'png', 'jpeg'
|
|
23
|
+
self.image path
|
|
24
|
+
when 'pdf'
|
|
25
|
+
self.pdf path
|
|
26
|
+
when 'auto'
|
|
27
|
+
else
|
|
28
|
+
raise ScoutException, "Unkown file type: #{parameters[:file_type]}"
|
|
29
|
+
end
|
|
30
|
+
rescue ScoutException => e
|
|
31
|
+
e
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
properties = {
|
|
36
|
+
file: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Path to the file'
|
|
39
|
+
},
|
|
40
|
+
file_type: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'File type of the file (e.g image, pdf), auto by default',
|
|
43
|
+
enum: ATACH_TYPES,
|
|
44
|
+
default: 'auto'
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
description = <<-EOF
|
|
49
|
+
Attach a file to the current chat, supports images and pdfs.
|
|
50
|
+
EOF
|
|
51
|
+
|
|
52
|
+
function = {
|
|
53
|
+
name: task_name,
|
|
54
|
+
description: description,
|
|
55
|
+
parameters: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: properties,
|
|
58
|
+
required: [:file],
|
|
59
|
+
additionalProperties: false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
definition = IndiferentHash.setup(function.merge(type: 'function', function: function))
|
|
64
|
+
@other_options[:tools][task_name] = [block, definition]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -7,6 +7,7 @@ module AgentWorkflow
|
|
|
7
7
|
@chat ||= begin
|
|
8
8
|
chat = recursive_inputs[:chat]
|
|
9
9
|
chat = Chat.parse(chat) if String === chat
|
|
10
|
+
chat = [] if chat.nil?
|
|
10
11
|
Chat.setup(chat)
|
|
11
12
|
chat
|
|
12
13
|
end
|
|
@@ -37,7 +38,7 @@ module AgentWorkflow
|
|
|
37
38
|
helper :agent do |name = nil, chat: nil, options: nil, tooling: nil, files: nil, **kwargs|
|
|
38
39
|
options = self.options if options.nil?
|
|
39
40
|
tooling = self.tooling if tooling.nil?
|
|
40
|
-
options = IndiferentHash.add_defaults
|
|
41
|
+
options = IndiferentHash.add_defaults kwargs, options
|
|
41
42
|
|
|
42
43
|
agent = LLM.load_agent name, agent_options(options)
|
|
43
44
|
agent.job = self
|
data/lib/scout/llm/agent.rb
CHANGED
|
@@ -104,6 +104,13 @@ module LLM
|
|
|
104
104
|
self.socialize(options.dup) if socialize && %w(true TRUE True T 1).include?(socialize.to_s)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
if (list = messages.select{|info| info[:role] == 'attachments'}).any?
|
|
108
|
+
attachments = list.last[:content]
|
|
109
|
+
messages.delete_if{|info| info[:role] == 'attachments' }
|
|
110
|
+
self.attachments if attachments && %w(true TRUE True T 1).include?(attachments.to_s)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
|
|
107
114
|
tools = options[:tools] || {}
|
|
108
115
|
if other_tools = @other_options[:tools]
|
|
109
116
|
other_tools = JSON.parse other_tools if String === other_tools
|
|
@@ -117,7 +124,7 @@ module LLM
|
|
|
117
124
|
end
|
|
118
125
|
|
|
119
126
|
options[:tools] = tools
|
|
120
|
-
LLM.ask messages, @other_options.except(:no_ask_override).merge(log_errors: true).merge(options).merge(agent: false)
|
|
127
|
+
LLM.ask messages, @other_options.except(:no_ask_override).merge(log_errors: true, save_file: self.save_file).merge(options).merge(agent: false)
|
|
121
128
|
end
|
|
122
129
|
rescue
|
|
123
130
|
exception = $!
|
|
@@ -219,5 +226,6 @@ end
|
|
|
219
226
|
require_relative 'agent/chat'
|
|
220
227
|
require_relative 'agent/iterate'
|
|
221
228
|
require_relative 'agent/delegate'
|
|
229
|
+
require_relative 'agent/attach'
|
|
222
230
|
require_relative 'agent/save'
|
|
223
231
|
require_relative 'agent/workflow'
|
data/lib/scout/llm/ask.rb
CHANGED
|
@@ -13,20 +13,9 @@ module LLM
|
|
|
13
13
|
messages = LLM.chat(question)
|
|
14
14
|
options = IndiferentHash.add_defaults options, LLM.options(messages)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
persist ||= Scout::Config.get :persist, :ask, :llm, env: 'ASK_PERSIST,LLM_PERSIST,PERSIST'
|
|
19
|
-
endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK'
|
|
20
|
-
if endpoint && Scout.etc.AI[endpoint].find_with_extension(:yaml).exists?
|
|
21
|
-
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
22
|
-
elsif endpoint && endpoint != ""
|
|
23
|
-
raise "Endpoint not found #{endpoint}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
agent_name = IndiferentHash.process_options options, :agent
|
|
16
|
+
agent_name, agent_save_file = IndiferentHash.process_options options, :agent, :agent_save_file
|
|
27
17
|
agent_name = nil if %(none false nil).include?(agent_name.to_s)
|
|
28
18
|
if agent_name
|
|
29
|
-
options[:endpoint] ||= endpoint
|
|
30
19
|
agent = LLM::Agent.load_agent agent_name
|
|
31
20
|
agent.save_file = agent_save_file if agent_save_file
|
|
32
21
|
agent.follow messages
|
|
@@ -34,6 +23,16 @@ module LLM
|
|
|
34
23
|
return res
|
|
35
24
|
end
|
|
36
25
|
|
|
26
|
+
endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
|
|
27
|
+
|
|
28
|
+
persist ||= Scout::Config.get :persist, :ask, :llm, env: 'ASK_PERSIST,LLM_PERSIST,PERSIST'
|
|
29
|
+
endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK'
|
|
30
|
+
if endpoint && Scout.etc.AI[endpoint].find_with_extension(:yaml).exists?
|
|
31
|
+
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
32
|
+
elsif endpoint && endpoint != ""
|
|
33
|
+
raise "Endpoint not found #{endpoint}"
|
|
34
|
+
end
|
|
35
|
+
|
|
37
36
|
job_paths = messages.job_paths
|
|
38
37
|
meta = Chat.meta(messages)
|
|
39
38
|
options[:current_meta] = meta if meta and meta.any?
|
|
@@ -481,8 +481,8 @@ module LLM
|
|
|
481
481
|
def ask(question, options = {}, &block)
|
|
482
482
|
original_options = options.dup
|
|
483
483
|
|
|
484
|
-
return_messages, log_response, current_meta, relay, process, prompt_strategies = IndiferentHash.process_options options,
|
|
485
|
-
:return_messages, :log_response, :current_meta, :relay, :process, :prompt_strategies,
|
|
484
|
+
return_messages, log_response, current_meta, relay, process, prompt_strategies, save_file = IndiferentHash.process_options options,
|
|
485
|
+
:return_messages, :log_response, :current_meta, :relay, :process, :prompt_strategies, :save_file,
|
|
486
486
|
return_messages: false, log_response: true
|
|
487
487
|
|
|
488
488
|
messages = self.messages question, options
|
|
@@ -507,6 +507,7 @@ module LLM
|
|
|
507
507
|
Log.debug 'Asking error. Options: ' + "\n" + JSON.pretty_generate(options.except(:tools))
|
|
508
508
|
begin
|
|
509
509
|
tmpfile = TmpFile.tmp_file
|
|
510
|
+
Open.write save_file + ".error", Chat.print(messages) if save_file
|
|
510
511
|
Open.write tmpfile + ".chat", Chat.print(messages)
|
|
511
512
|
Open.write tmpfile + ".options", options.except(:messages, :tools).to_json
|
|
512
513
|
Open.write tmpfile + ".meta", current_meta.to_json
|
|
@@ -517,6 +518,8 @@ module LLM
|
|
|
517
518
|
end
|
|
518
519
|
|
|
519
520
|
raise e
|
|
521
|
+
ensure
|
|
522
|
+
Open.write save_file, Chat.print(messages) if save_file
|
|
520
523
|
end
|
|
521
524
|
end
|
|
522
525
|
|
|
@@ -544,6 +547,7 @@ module LLM
|
|
|
544
547
|
if previous_response_id_error
|
|
545
548
|
message.unshift IndiferentHash.setup({role: :previous_response_id, content: previous_response_id_error})
|
|
546
549
|
end
|
|
550
|
+
Open.write save_file + ".error", Chat.print(messages) if save_file
|
|
547
551
|
Open.write tmpfile + ".chat", Chat.print(messages)
|
|
548
552
|
Open.write tmpfile + ".options", options.except(:messages, :tools).to_json
|
|
549
553
|
Open.write tmpfile + ".meta", current_meta.to_json
|
|
@@ -553,6 +557,8 @@ module LLM
|
|
|
553
557
|
rescue
|
|
554
558
|
end
|
|
555
559
|
raise e
|
|
560
|
+
ensure
|
|
561
|
+
Open.write save_file, Chat.print(messages) if save_file
|
|
556
562
|
end
|
|
557
563
|
|
|
558
564
|
if log_response
|
|
@@ -561,7 +567,7 @@ module LLM
|
|
|
561
567
|
meta['timestamp'] = timestamp
|
|
562
568
|
end
|
|
563
569
|
|
|
564
|
-
output = chain_tools messages, output, tools, options.merge(client: client, tools: tools, log_response: log_response, current_meta: meta, relay: relay)
|
|
570
|
+
output = chain_tools messages, output, tools, options.merge(client: client, tools: tools, log_response: log_response, current_meta: meta, relay: relay, save_file: save_file)
|
|
565
571
|
|
|
566
572
|
if log_response && meta && meta.any?
|
|
567
573
|
# The meta is about to become the first message of a segment that
|
|
@@ -194,14 +194,11 @@ module Chat
|
|
|
194
194
|
workflow = Workflow.require_workflow workflow_name
|
|
195
195
|
end unless workflow
|
|
196
196
|
|
|
197
|
-
raise "Workflow
|
|
197
|
+
raise ScoutException, "Workflow '#{workflow_name}' not found" if workflow.nil?
|
|
198
198
|
|
|
199
199
|
next if workflow.documentation.empty?
|
|
200
200
|
content = <<-EOF
|
|
201
|
-
|
|
202
|
-
Below is the documentation of the workflow:
|
|
203
|
-
|
|
204
|
-
# #{workflow.documentation[:title]}
|
|
201
|
+
# Documentation for the '#{workflow_name}' workflow: #{workflow.documentation[:title]}
|
|
205
202
|
|
|
206
203
|
#{workflow.documentation[:description]}
|
|
207
204
|
EOF
|
|
@@ -95,6 +95,55 @@ module Chat
|
|
|
95
95
|
[kind.to_sym, provenance_path(kind, object)]
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# Run-scoped, parse-once chat cache (review theme 04, stage s1)
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# A provenance "run" is one top-level call to a public provenance entry
|
|
102
|
+
# point (traverse_provenance, provenance_token_events,
|
|
103
|
+
# provenance_token_totals / tokens, and the collectors delegating to them).
|
|
104
|
+
# Inside a run every chat file is parsed at most once, keyed by the same
|
|
105
|
+
# realpath discipline as node identity (provenance_path), so a file reached
|
|
106
|
+
# through several addresses - symlinks, or a chat-typed job whose result
|
|
107
|
+
# path equals a chat node path - is parsed once and never double counted.
|
|
108
|
+
#
|
|
109
|
+
# Guards:
|
|
110
|
+
# * lifetime: the cache exists only while a run is active and is dropped
|
|
111
|
+
# when it ends, so a live session that appends between two runs is
|
|
112
|
+
# always re-parsed (no cross-run persistence);
|
|
113
|
+
# * identity: realpath keyed, exactly like provenance nodes;
|
|
114
|
+
# * transparency: no envelope shape, traversal order or deduplication
|
|
115
|
+
# semantics change - the same Chat object is simply reused within the
|
|
116
|
+
# run, and everything downstream only reads it.
|
|
117
|
+
def self.with_provenance_run_cache
|
|
118
|
+
previous = Thread.current[:scout_ai_provenance_run_cache]
|
|
119
|
+
Thread.current[:scout_ai_provenance_run_cache] ||= {}
|
|
120
|
+
begin
|
|
121
|
+
yield
|
|
122
|
+
ensure
|
|
123
|
+
Thread.current[:scout_ai_provenance_run_cache] = previous
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Manual scope control for linear, top-level callers such as the `prov`
|
|
128
|
+
# SOPT script, whose body cannot be wrapped in a block without reindenting
|
|
129
|
+
# the whole file. Open at the start of the run, close when it ends; an
|
|
130
|
+
# open scope makes nested with_provenance_run_cache calls reuse it.
|
|
131
|
+
def self.open_provenance_run_cache
|
|
132
|
+
Thread.current[:scout_ai_provenance_run_cache] ||= {}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.close_provenance_run_cache
|
|
136
|
+
Thread.current[:scout_ai_provenance_run_cache] = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Chat.load with the run cache applied. Outside a run this parses
|
|
140
|
+
# directly, exactly like Chat.load.
|
|
141
|
+
def self.provenance_chat_load(path)
|
|
142
|
+
cache = Thread.current[:scout_ai_provenance_run_cache]
|
|
143
|
+
return Chat.load(path) unless cache
|
|
144
|
+
cache[provenance_path(:chat, path)] ||= Chat.load(path)
|
|
145
|
+
end
|
|
146
|
+
|
|
98
147
|
def self.provenance_error(on_error, error, kind, object, relation, reference)
|
|
99
148
|
raise error unless on_error
|
|
100
149
|
on_error.call(error, kind, object, relation, reference)
|
|
@@ -206,6 +255,11 @@ module Chat
|
|
|
206
255
|
# therefore never follows import, continue, or last references.
|
|
207
256
|
def self.traverse_provenance(root, root_type: nil, follow: :all, on_error: nil, &block)
|
|
208
257
|
return enum_for(__method__, root, root_type: root_type, follow: follow, on_error: on_error) unless block
|
|
258
|
+
unless Thread.current[:scout_ai_provenance_run_cache]
|
|
259
|
+
return with_provenance_run_cache do
|
|
260
|
+
traverse_provenance(root, root_type: root_type, follow: follow, on_error: on_error, &block)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
209
263
|
# Lambda blocks have strict arity; keep six-argument callbacks compatible
|
|
210
264
|
# by only yielding the trailing detail when the block can receive it.
|
|
211
265
|
detail_arity = lambda do
|
|
@@ -244,7 +298,7 @@ module Chat
|
|
|
244
298
|
|
|
245
299
|
begin
|
|
246
300
|
if kind == :chat
|
|
247
|
-
chat =
|
|
301
|
+
chat = provenance_chat_load(object)
|
|
248
302
|
|
|
249
303
|
if relations.include?(:job)
|
|
250
304
|
chat.jobs.each do |reference|
|
|
@@ -497,6 +551,11 @@ module Chat
|
|
|
497
551
|
#
|
|
498
552
|
# Checkpoint fields (*_c, *_s) are never read or summed here.
|
|
499
553
|
def self.provenance_token_events(root, warnings: nil, strict: false, **traversal_options)
|
|
554
|
+
unless Thread.current[:scout_ai_provenance_run_cache]
|
|
555
|
+
return with_provenance_run_cache do
|
|
556
|
+
provenance_token_events(root, warnings: warnings, strict: strict, **traversal_options)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
500
559
|
# Route traversal-stage agent_meta problems into the caller's warnings
|
|
501
560
|
# Array instead of letting them raise. Other traversal errors stay strict
|
|
502
561
|
# (raise), and an explicitly supplied on_error keeps being called.
|
|
@@ -519,7 +578,7 @@ module Chat
|
|
|
519
578
|
|
|
520
579
|
files = provenance_chat_files(root, **traversal_options)
|
|
521
580
|
sources = {}
|
|
522
|
-
files.each { |file| sources[file] =
|
|
581
|
+
files.each { |file| sources[file] = provenance_chat_load(file) }
|
|
523
582
|
|
|
524
583
|
evidences = []
|
|
525
584
|
|