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.
@@ -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, and the live part goes
10
- # on from the kept rounds (Resumed): its first request is a later round,
11
- # and a pause for budget keeps the kept rounds too.
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, and the
17
- # rounds its kept messages made.
18
- Resumed = Data.define(:start, :rounds)
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.fetch(:messages)) if 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, Resumed.new(start:, rounds: kept_rounds(kept)), &)
36
+ recorded_completion(chat, memo, key, resumed(start, kept), &)
33
37
  end
34
38
 
35
- def kept_rounds(kept) = kept ? kept.fetch(:messages).count { _1.role == :assistant } : 0
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, messages)
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
- Tool.current_tool_execution_tracker&.mark_started! if messages.any? { _1.role == :tool }
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
- live = chat.messages.length
50
- result = ResponseMemo.recording(chat_provider(chat), memo, key, resumed.start) { yield(resumed) }
51
- ResponseMemo.store(memo, key, chat.messages.drop(resumed.start), complete: true,
52
- facts: streamed_facts(chat, live, result))
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
- def streamed_facts(chat, live, result)
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 = chat.messages.drop(live).select { _1.role == :assistant }
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)
@@ -54,6 +54,7 @@ module Smith
54
54
  end
55
55
  [output, nil]
56
56
  rescue StandardError => e
57
+ Interruptions.pass!(e)
57
58
  [nil, e]
58
59
  end
59
60
 
@@ -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.0
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