smith-agents 0.12.1 → 0.12.3
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/CHANGELOG.md +90 -0
- data/README.md +197 -1081
- data/docs/AGENTS.md +241 -0
- data/docs/BUDGETS.md +134 -0
- data/docs/CONFIGURATION.md +29 -3
- data/docs/DEVELOPMENT.md +63 -0
- data/docs/ERRORS_AND_RETRIES.md +177 -0
- data/docs/INSPECTION.md +39 -0
- data/docs/PATTERNS.md +2 -2
- data/docs/PERSISTENCE.md +175 -4
- data/docs/TESTING.md +88 -0
- data/docs/TOOLS_AND_GUARDRAILS.md +59 -1
- data/lib/smith/agent/completion.rb +5 -3
- data/lib/smith/agent/provider_completion.rb +1 -1
- data/lib/smith/agent/provider_streaming.rb +15 -9
- data/lib/smith/agent/usage_tracking.rb +1 -2
- data/lib/smith/batch.rb +11 -9
- data/lib/smith/cache_markers.rb +86 -0
- data/lib/smith/interruptions.rb +50 -0
- data/lib/smith/launchers/inline.rb +1 -0
- data/lib/smith/providers/request_holds.rb +1 -1
- data/lib/smith/providers/stream_facts.rb +29 -11
- data/lib/smith/response_memo.rb +150 -45
- data/lib/smith/setting_constructors.rb +14 -0
- data/lib/smith/tool/chat_execution_callbacks.rb +1 -1
- data/lib/smith/tool/execution_batch.rb +5 -0
- data/lib/smith/tool/execution_failure_handling.rb +27 -9
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/deterministic_execution.rb +5 -1
- data/lib/smith/workflow/parallel.rb +5 -1
- data/lib/smith/workflow/persistence.rb +4 -1
- data/lib/smith/workflow/remembered_calls.rb +52 -19
- data/lib/smith/workflow/retry_execution.rb +1 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +1 -0
- data/lib/smith/workflow/step_context.rb +1 -0
- data/lib/smith/workflow.rb +4 -2
- data/lib/smith.rb +10 -0
- metadata +9 -1
|
@@ -6,16 +6,20 @@ module Smith
|
|
|
6
6
|
# (Smith::ResponseMemo): each call takes its kept messages, if any, and
|
|
7
7
|
# sends nothing when they end it (a streamed step's sink gets the reply's
|
|
8
8
|
# text at once), else goes on from them, and is recorded as it goes.
|
|
9
|
-
# Kept tool results count as tools the step ran,
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Kept tool results count as tools the step ran, charged to its
|
|
10
|
+
# tool-call allowances as they were before the cut; a kept reply whose
|
|
11
|
+
# tools had not run is the answer to the live part's first request, and
|
|
12
|
+
# its tools run. The live part goes on from the kept rounds (Resumed):
|
|
13
|
+
# its first request is a later round, a pause for budget keeps the kept
|
|
14
|
+
# rounds too, and a streamed one's facts pair with its live replies.
|
|
12
15
|
module RememberedCalls
|
|
13
16
|
CALLS_LOCK = Mutex.new
|
|
14
17
|
private_constant :CALLS_LOCK
|
|
15
18
|
|
|
16
|
-
# Where a call's live part starts: the call's first message,
|
|
17
|
-
# rounds its kept messages made
|
|
18
|
-
|
|
19
|
+
# Where a call's live part starts: the call's first message, the
|
|
20
|
+
# rounds its kept messages made, and its kept reply whose tools had
|
|
21
|
+
# not run (one of those rounds).
|
|
22
|
+
Resumed = Data.define(:start, :rounds, :pending)
|
|
19
23
|
|
|
20
24
|
private
|
|
21
25
|
|
|
@@ -26,39 +30,68 @@ module Smith
|
|
|
26
30
|
|
|
27
31
|
start = chat.messages.length
|
|
28
32
|
kept = ResponseMemo.fetch(memo, key)
|
|
29
|
-
keep_messages(chat, kept
|
|
33
|
+
keep_messages(chat, kept) if kept
|
|
30
34
|
return replayed_completion(chat, model_reference) if kept&.fetch(:complete)
|
|
31
35
|
|
|
32
|
-
recorded_completion(chat, memo, key,
|
|
36
|
+
recorded_completion(chat, memo, key, resumed(start, kept), &)
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
def
|
|
39
|
+
def resumed(start, kept)
|
|
40
|
+
return Resumed.new(start:, rounds: 0, pending: nil) unless kept
|
|
41
|
+
|
|
42
|
+
pending = kept.fetch(:pending)
|
|
43
|
+
Resumed.new(start:, rounds: kept.fetch(:messages).count { _1.role == :assistant } + (pending ? 1 : 0), pending:)
|
|
44
|
+
end
|
|
36
45
|
|
|
37
46
|
def memo_key(chat, facts)
|
|
38
47
|
ResponseMemo.key([*@memo_attempts, @step_attempt.to_i], facts[:attempt_index], next_memo_call, chat)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# The kept messages, after the call's own; their tool results are
|
|
42
|
-
# tools the step ran.
|
|
43
|
-
def keep_messages(chat,
|
|
51
|
+
# tools the step ran, each charged as it was.
|
|
52
|
+
def keep_messages(chat, kept)
|
|
53
|
+
messages = kept.fetch(:messages)
|
|
44
54
|
messages.each { chat.add_message(_1) }
|
|
45
|
-
|
|
55
|
+
ran = messages.filter_map { _1.tool_call_id if _1.role == :tool }
|
|
56
|
+
Tool.current_tool_execution_tracker&.mark_started! if ran.any?
|
|
57
|
+
charge_kept_calls(ran_calls(messages, ran))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ran_calls(messages, ran) = messages.flat_map { _1.tool_calls&.values.to_a }.select { ran.include?(_1.id) }
|
|
61
|
+
|
|
62
|
+
# Each kept call against the agent's tool-call allowance and the
|
|
63
|
+
# workflow's `tool_calls` budget, as a call that runs is, so a replay
|
|
64
|
+
# runs no more tools than the step may. A call past them (a budget
|
|
65
|
+
# lowered since, or a call rejected for budget) charges nothing more.
|
|
66
|
+
def charge_kept_calls(calls)
|
|
67
|
+
allowance = Tool.current_tool_call_allowance
|
|
68
|
+
ledger = Tool.current_ledger
|
|
69
|
+
commit = -> { ledger.reconcile!(ledger.reserve!(:tool_calls, 1), 1) if ledger&.limits&.key?(:tool_calls) }
|
|
70
|
+
calls.each { allowance.is_a?(Tool::CallAllowance) ? allowance.charge!(_1.name, &commit) : commit.call }
|
|
71
|
+
rescue BudgetExceeded
|
|
72
|
+
nil
|
|
46
73
|
end
|
|
47
74
|
|
|
75
|
+
# A call a tool halted ends in that tool's result, not a reply: it
|
|
76
|
+
# keeps its progress, which a replay can go on from.
|
|
48
77
|
def recorded_completion(chat, memo, key, resumed)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
78
|
+
result = ResponseMemo.recording(chat_provider(chat), memo, key, resumed.start, resumed.pending) do
|
|
79
|
+
yield(resumed)
|
|
80
|
+
end
|
|
81
|
+
messages = chat.messages.drop(resumed.start)
|
|
82
|
+
if ResponseMemo.answer?(messages.last)
|
|
83
|
+
ResponseMemo.store(memo, key, messages, complete: true, facts: streamed_facts(messages, result))
|
|
84
|
+
end
|
|
53
85
|
result
|
|
54
86
|
end
|
|
55
87
|
|
|
56
|
-
# A streamed call's replies' facts, read from its stream
|
|
57
|
-
|
|
88
|
+
# A streamed call's replies' facts, read from its stream (its kept
|
|
89
|
+
# replies, which no round of it answered, keep their own).
|
|
90
|
+
def streamed_facts(messages, result)
|
|
58
91
|
stream = result[1]
|
|
59
92
|
return {} unless stream.is_a?(Hash) && stream[:rounds]
|
|
60
93
|
|
|
61
|
-
replies =
|
|
94
|
+
replies = messages.select { _1.role == :assistant }
|
|
62
95
|
replies.zip(Providers::StreamFacts.per_message(replies, stream[:rounds], stream[:output_limit]))
|
|
63
96
|
.to_h.compare_by_identity
|
|
64
97
|
end
|
|
@@ -39,6 +39,7 @@ module Smith
|
|
|
39
39
|
attempt_scope = attempt_idempotency_scope(key_generation)
|
|
40
40
|
run_attempt_under_keys(transition, attempt_scope)
|
|
41
41
|
rescue StandardError => e
|
|
42
|
+
Interruptions.pass!(e)
|
|
42
43
|
raise unless retry_transition_error?(config, e, attempt)
|
|
43
44
|
|
|
44
45
|
key_generation += 1 if new_idempotency_key_after?(config, e, attempt_scope)
|
|
@@ -16,6 +16,7 @@ module Smith
|
|
|
16
16
|
install_step_idempotency_scope(transition)
|
|
17
17
|
Thread.handle_interrupt(Object => :immediate, &block)
|
|
18
18
|
rescue StandardError => e
|
|
19
|
+
Interruptions.pass!(e)
|
|
19
20
|
@outcome = nil
|
|
20
21
|
GuardrailIntegration.instance_method(:handle_step_failure).bind_call(self, transition, e)
|
|
21
22
|
ensure
|
data/lib/smith/workflow.rb
CHANGED
|
@@ -412,10 +412,12 @@ module Smith
|
|
|
412
412
|
normalize_seed_messages(seeded)
|
|
413
413
|
end
|
|
414
414
|
|
|
415
|
-
|
|
415
|
+
# A seed's drift digest, its parts' cache flags aside (CacheMarkers);
|
|
416
|
+
# with them, as Smith before 0.12.3 took it.
|
|
417
|
+
def compute_seed_digest(messages, cache_flags: false)
|
|
416
418
|
return nil if messages.nil? || messages.empty?
|
|
417
419
|
|
|
418
|
-
Digest::SHA256.hexdigest(JSON.generate(messages))
|
|
420
|
+
Digest::SHA256.hexdigest(JSON.generate(cache_flags ? messages : CacheMarkers.strip_parts(messages)))
|
|
419
421
|
rescue JSON::GeneratorError, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
|
|
420
422
|
raise WorkflowError,
|
|
421
423
|
"seed_messages content must be valid UTF-8 (Smith hashes via JSON.generate " \
|
data/lib/smith.rb
CHANGED
|
@@ -79,6 +79,14 @@ module Smith
|
|
|
79
79
|
:response_memo, { fetch: "fetch(key)", store: "store(key, record)" }
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
+
# Errors that interrupt a step instead of failing it, such as the host's
|
|
83
|
+
# database or queue out of reach (Smith::Interruptions). A step that meets
|
|
84
|
+
# one, raised in it or as the cause of an error that was, stops where it
|
|
85
|
+
# is, as if its worker had stopped: no failure route, no retry, no failure
|
|
86
|
+
# recorded, and the error reaches the caller as it was raised.
|
|
87
|
+
setting :interrupting_errors, default: [].freeze,
|
|
88
|
+
constructor: SettingConstructors.error_classes(:interrupting_errors)
|
|
89
|
+
|
|
82
90
|
# How long a provider request's hold on a host's budget ledger
|
|
83
91
|
# (`budget_ledger`) lasts unsettled, in seconds: past it the request's
|
|
84
92
|
# process is taken to have died, and the ledger drops the hold. It must
|
|
@@ -203,6 +211,7 @@ require_relative "smith/providers/schema_constraints"
|
|
|
203
211
|
require_relative "smith/tool_failure_notification_failed"
|
|
204
212
|
require_relative "smith/persistence_payload_conflict"
|
|
205
213
|
require_relative "smith/exponential_backoff"
|
|
214
|
+
require_relative "smith/interruptions"
|
|
206
215
|
|
|
207
216
|
# Event system (depends on Types)
|
|
208
217
|
require_relative "smith/event"
|
|
@@ -218,6 +227,7 @@ require_relative "smith/budget/ledger"
|
|
|
218
227
|
require_relative "smith/budget/host_ledger"
|
|
219
228
|
require_relative "smith/budget/request_hold"
|
|
220
229
|
require_relative "smith/budget/continuation"
|
|
230
|
+
require_relative "smith/cache_markers"
|
|
221
231
|
require_relative "smith/response_memo"
|
|
222
232
|
|
|
223
233
|
# Pricing (depends on Smith config)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smith-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -143,9 +143,15 @@ files:
|
|
|
143
143
|
- README.md
|
|
144
144
|
- Rakefile
|
|
145
145
|
- UPSTREAM_PROPOSAL.md
|
|
146
|
+
- docs/AGENTS.md
|
|
147
|
+
- docs/BUDGETS.md
|
|
146
148
|
- docs/CONFIGURATION.md
|
|
149
|
+
- docs/DEVELOPMENT.md
|
|
150
|
+
- docs/ERRORS_AND_RETRIES.md
|
|
151
|
+
- docs/INSPECTION.md
|
|
147
152
|
- docs/PATTERNS.md
|
|
148
153
|
- docs/PERSISTENCE.md
|
|
154
|
+
- docs/TESTING.md
|
|
149
155
|
- docs/TOOLS_AND_GUARDRAILS.md
|
|
150
156
|
- docs/workflow_claim.md
|
|
151
157
|
- exe/smith
|
|
@@ -213,6 +219,7 @@ files:
|
|
|
213
219
|
- lib/smith/budget/reservation.rb
|
|
214
220
|
- lib/smith/budget/reservation_contract.rb
|
|
215
221
|
- lib/smith/budget/spent_reservation.rb
|
|
222
|
+
- lib/smith/cache_markers.rb
|
|
216
223
|
- lib/smith/child_run.rb
|
|
217
224
|
- lib/smith/child_runs_failed.rb
|
|
218
225
|
- lib/smith/cli.rb
|
|
@@ -255,6 +262,7 @@ files:
|
|
|
255
262
|
- lib/smith/guardrails/url_verifier.rb
|
|
256
263
|
- lib/smith/idempotency_key.rb
|
|
257
264
|
- lib/smith/idempotency_key/canonical.rb
|
|
265
|
+
- lib/smith/interruptions.rb
|
|
258
266
|
- lib/smith/launchers/inline.rb
|
|
259
267
|
- lib/smith/message.rb
|
|
260
268
|
- lib/smith/models.rb
|