smith-agents 0.12.0 → 0.12.2
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 +77 -0
- data/README.md +197 -1080
- data/docs/AGENTS.md +238 -0
- data/docs/BUDGETS.md +134 -0
- data/docs/CONFIGURATION.md +30 -4
- data/docs/DEVELOPMENT.md +63 -0
- data/docs/ERRORS_AND_RETRIES.md +175 -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/generators/smith/install/install_generator.rb +6 -2
- 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/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 +149 -44
- 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/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.rb +9 -0
- metadata +8 -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.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"
|
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.2
|
|
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
|
|
@@ -255,6 +261,7 @@ files:
|
|
|
255
261
|
- lib/smith/guardrails/url_verifier.rb
|
|
256
262
|
- lib/smith/idempotency_key.rb
|
|
257
263
|
- lib/smith/idempotency_key/canonical.rb
|
|
264
|
+
- lib/smith/interruptions.rb
|
|
258
265
|
- lib/smith/launchers/inline.rb
|
|
259
266
|
- lib/smith/message.rb
|
|
260
267
|
- lib/smith/models.rb
|