smith-agents 0.13.0 → 0.13.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22c04af0f6062eb36758bb0fedc62663d68a27dce67f016942038ffc1ee3cc07
4
- data.tar.gz: 0243fe30beb96d8bccc37869cb6ece9a3525ecd83004de98e18d8f97f4de8b8c
3
+ metadata.gz: 7c3bc332fe08b2ff077b3039603ddc8356ac65d5034f47abcf4253c3e167f8c6
4
+ data.tar.gz: 9eaf0e8548b473da25838206b63e2a5639d7ee4391f7cb6e30980a5b5aa46e67
5
5
  SHA512:
6
- metadata.gz: 41b4452c51e7aa3e6946c192e0f6a9eb9bd89163c90886f013dfffd891da55a53f40ce30b00eb90ecebdc5adab3f7373687c7e0ae64be3e32085d6bfc05af47d
7
- data.tar.gz: 4007dfee19e37843c993cf9abf5ea57a6e7450141594090fb0579ae013437de6e3053de16c382a88180bb5536b84fdbe951270e0e6707e7a0c73daec9036a569
6
+ metadata.gz: 1ff7f020acb2efd11f7143a074d3c65093137be5bc2646e10a668e1e5242b37255db15909f0c99aa3b6073ad73e31a2557f61fa3d2a35d2de716ba122d7deb4e
7
+ data.tar.gz: fab4fd19cd33c21b6800a8d75740bc62533aa4100a2d9ae5e26bad31ad5a1fa57b4869cce9efc9a351b41366ee5ee5a8cb24c0d21dc8369effdbe5b8fd8e703c
data/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ All notable changes to Smith are documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
6
6
 
7
+ ## [0.13.1] - 2026-11-08
8
+
9
+ ### Changed
10
+
11
+ - Split-step persistence refuses a step declared `from: nil` when it is
12
+ prepared: `prepare_persisted_step!` raises `Smith::WorkflowError` naming
13
+ the step and saying to run it with `advance_persisted!`, before anything
14
+ is written, as a prepared step is checked against the state its step is
15
+ declared from. It is not supported yet. Under 0.13.0 such a step was
16
+ prepared, which stores its run as a step in progress, and then failed:
17
+ its work ran first, a parallel or fan-out step's branches included, and
18
+ then "prepared execution did not return the claimed transition", so a
19
+ retry ran the work again. Only a parallel or fan-out step whose branches
20
+ the host ran through `prepare_composite_step!` failed in that call,
21
+ before any branch ran, its run left in progress all the same. A run
22
+ stored that way still raises `Smith::StepInProgressOnRestore` in strict
23
+ mode.
24
+
25
+ ### Fixed
26
+
27
+ - A failure in a step declared `from: nil` (one that runs from whichever
28
+ state routes to it) restores. Smith records the failure with the step's
29
+ declared origin, `from: nil`, as its step result, its `StepFailed` event
30
+ and its trace have it, but restore refused a record without a `from`
31
+ state (`Smith::PersistedFailureInvalid`), so a stored run could not go on
32
+ to the step's `on_failure`, and a run that ended failed there could not
33
+ be read back. Restore now takes a nil `from`, in lax and strict modes and
34
+ through every store adapter, and a run stored by 0.13.0 restores as it
35
+ is. Any other `from`, and its `transition` and `to`, must still be text
36
+ of 1 to 256 bytes, and a record without the field is still refused.
37
+ - A run whose last failed step was renamed (`renamed_transition`, or
38
+ `renamed_state` for the state it failed from or was headed to) restores.
39
+ The names a rename leads to are US-ASCII text, as a Symbol's name is,
40
+ and restore refused any failure record name not marked UTF-8
41
+ (`Smith::PersistedFailureInvalid`, "transition is invalid"), so such a
42
+ run could not go on to the step's `on_failure`, or be read back once it
43
+ ended failed. A failure record restored from `to_state` itself, without a
44
+ JSON round trip, was refused the same way. US-ASCII text is UTF-8 as it
45
+ stands and is now taken as such; text in any other encoding, or with
46
+ bytes outside its encoding, is still refused. Runs stored by 0.12.7 and
47
+ later restore as they are.
48
+
49
+ ### Upgrading
50
+
51
+ - `Smith::EXECUTION_SEMANTICS_VERSION` stays `"7"`: no persisted state,
52
+ wait, pending retry or durable plan changes shape, and a run stored by
53
+ 0.13.0 restores as it is.
54
+
7
55
  ## [0.13.0] - 2026-11-01
8
56
 
9
57
  ### Added
data/README.md CHANGED
@@ -30,7 +30,7 @@ What you get:
30
30
 
31
31
  ```ruby
32
32
  # Gemfile
33
- gem "smith-agents", "0.13.0", require: "smith"
33
+ gem "smith-agents", "0.13.1", require: "smith"
34
34
  ```
35
35
 
36
36
  In a Rails app:
data/docs/PERSISTENCE.md CHANGED
@@ -60,6 +60,11 @@ What Smith does for you:
60
60
  not the caller's Hash
61
61
  - persists accepted session history
62
62
  - preserves chosen next transitions across persistence
63
+ - keeps a step's failure across persistence: a restored run takes the step's
64
+ `on_failure`, and `RunResult#last_error` and `#failure_detail` read as they
65
+ did in the run that failed. A step declared `from: nil`, which runs from
66
+ whichever state routes to it, keeps `from: nil` there, as its step result
67
+ and its `StepFailed` event do
63
68
  - supports JSON host round-trips through `to_state` and `.from_state`
64
69
  - refuses to restore a payload whose state, or whose pending transition, the
65
70
  workflow class does not know (a deploy renamed or removed it, or the payload
@@ -307,6 +312,11 @@ a versioned persistence adapter whose `store_versioned` method accepts `ttl:`,
307
312
  and non-expiring workflow persistence. Smith explicitly pins `ttl: nil` for
308
313
  every boundary write, so a later global configuration change cannot turn an
309
314
  accepted checkpoint into expiring state.
315
+ It does not run a step declared `from: nil` yet: `prepare_persisted_step!`
316
+ refuses one with a `Smith::WorkflowError` naming the step, before anything is
317
+ written, since a prepared step is checked against the state its step is
318
+ declared from. The run itself is unchanged: run that step with
319
+ `advance_persisted!`.
310
320
  `prepare_persisted_step!` writes the normal `step_in_progress` marker and returns
311
321
  the pending transition name without consuming it. Duplicate preparation on the
312
322
  same workflow object is rejected. The boundary is pinned to the exact
data/lib/smith/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smith
4
- VERSION = "0.13.0"
4
+ VERSION = "0.13.1"
5
5
  EXECUTION_SEMANTICS_VERSION = "7"
6
6
  end
@@ -45,10 +45,12 @@ module Smith
45
45
  identity_attributes(values).merge(error_attributes(values)).freeze
46
46
  end
47
47
 
48
+ # A step declared `from: nil` is recorded with that origin, as its
49
+ # step result is; any other `from` must be text of 1 to 256 bytes.
48
50
  def identity_attributes(values)
49
51
  {
50
52
  transition: normalize_identifier(values.fetch(:transition), transition_normalizer, "transition"),
51
- from: normalize_identifier(values.fetch(:from), state_normalizer, "from state"),
53
+ from: optional_identifier(values.fetch(:from), state_normalizer, "from state"),
52
54
  to: normalize_identifier(values.fetch(:to), state_normalizer, "to state")
53
55
  }
54
56
  end
@@ -98,6 +100,10 @@ module Smith
98
100
  bounded_string(value, 256, label).then { normalizer.call(_1) }
99
101
  end
100
102
 
103
+ def optional_identifier(value, normalizer, label)
104
+ value.nil? ? nil : normalize_identifier(value, normalizer, label)
105
+ end
106
+
101
107
  def normalize_kind(value)
102
108
  return if value.nil?
103
109
 
@@ -15,11 +15,13 @@ module Smith
15
15
  MODULE_MATCH = Module.instance_method(:===)
16
16
  STRING_BYTESIZE = String.instance_method(:bytesize)
17
17
  STRING_ENCODING = String.instance_method(:encoding)
18
+ STRING_FORCE_ENCODING = String.instance_method(:force_encoding)
18
19
  STRING_INITIALIZE_COPY = String.instance_method(:initialize_copy)
19
20
  STRING_VALID_ENCODING = String.instance_method(:valid_encoding?)
20
21
  SYMBOL_TO_S = Symbol.instance_method(:to_s)
21
- private_constant :MODULE_MATCH, :STRING_BYTESIZE, :STRING_ENCODING, :STRING_INITIALIZE_COPY,
22
- :STRING_VALID_ENCODING, :SYMBOL_TO_S
22
+ TEXT_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
23
+ private_constant :MODULE_MATCH, :STRING_BYTESIZE, :STRING_ENCODING, :STRING_FORCE_ENCODING,
24
+ :STRING_INITIALIZE_COPY, :STRING_VALID_ENCODING, :SYMBOL_TO_S, :TEXT_ENCODINGS
23
25
 
24
26
  extend Dry::Initializer
25
27
 
@@ -33,8 +35,7 @@ module Smith
33
35
  end
34
36
 
35
37
  def call
36
- text = owned_text
37
- validate_encoding!(text)
38
+ text = utf8_text(owned_text)
38
39
  size = STRING_BYTESIZE.bind_call(text)
39
40
  return normalized(text, size) if normalize_length
40
41
  raise PersistedFailureInvalid, "persisted workflow failure #{label} is invalid" unless size.between?(1, limit)
@@ -44,11 +45,16 @@ module Smith
44
45
 
45
46
  private
46
47
 
47
- def validate_encoding!(text)
48
- valid = STRING_VALID_ENCODING.bind_call(text) && STRING_ENCODING.bind_call(text) == Encoding::UTF_8
49
- return if valid
48
+ # US-ASCII text is UTF-8 text as it stands (a Symbol's name and a
49
+ # renamed state's or step's name are US-ASCII), so its owned copy is
50
+ # marked UTF-8; any other encoding, or bytes outside it, is refused.
51
+ def utf8_text(text)
52
+ encoding = STRING_ENCODING.bind_call(text)
53
+ unless STRING_VALID_ENCODING.bind_call(text) && TEXT_ENCODINGS.include?(encoding)
54
+ raise PersistedFailureInvalid, "persisted workflow failure #{label} is invalid"
55
+ end
50
56
 
51
- raise PersistedFailureInvalid, "persisted workflow failure #{label} is invalid"
57
+ encoding == Encoding::UTF_8 ? text : STRING_FORCE_ENCODING.bind_call(text, Encoding::UTF_8)
52
58
  end
53
59
 
54
60
  # Message fields mirror capture normalization instead of rejecting on
@@ -65,14 +71,16 @@ module Smith
65
71
  end
66
72
 
67
73
  def owned_text
68
- return owned_string if string?
69
- return SYMBOL_TO_S.bind_call(value) if symbol?
74
+ return owned_copy(value) if string?
75
+ return owned_copy(SYMBOL_TO_S.bind_call(value)) if symbol?
70
76
 
71
77
  raise PersistedFailureInvalid, "persisted workflow failure #{label} must be text"
72
78
  end
73
79
 
74
- def owned_string
75
- String.allocate.tap { STRING_INITIALIZE_COPY.bind_call(_1, value) }
80
+ # A copy of its own, so marking it UTF-8 changes neither the caller's
81
+ # String nor the String a Symbol's name comes as (chilled, or frozen).
82
+ def owned_copy(source)
83
+ String.allocate.tap { STRING_INITIALIZE_COPY.bind_call(_1, source) }
76
84
  end
77
85
 
78
86
  def string? = MODULE_MATCH.bind_call(String, value)
@@ -76,9 +76,7 @@ module Smith
76
76
  transition_name = @next_transition_name || transition&.name
77
77
  raise WorkflowError, "workflow has no pending transition" unless transition_name
78
78
 
79
- refusal = "split-step persistence does not run steps that wait or launch runs"
80
- raise WorkflowError, refusal if transition&.waits_or_launches?
81
-
79
+ refuse_unsupported_split_step!(transition)
82
80
  ensure_transition_budget!
83
81
  resolved_key = normalize_split_step_persistence_key(candidate_split_step_persistence_key(key))
84
82
  store = persistence_adapter!(adapter)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ # The steps split-step persistence does not run, refused when one is
7
+ # prepared, before anything is written.
8
+ module UnsupportedSteps
9
+ private
10
+
11
+ def refuse_unsupported_split_step!(transition)
12
+ return unless transition
13
+
14
+ if transition.waits_or_launches?
15
+ raise WorkflowError, "split-step persistence does not run steps that wait or launch runs"
16
+ end
17
+ return unless transition.from.nil?
18
+
19
+ # A prepared step is checked against the state its step is declared
20
+ # from, which a step declared `from: nil` does not have; not
21
+ # supported yet.
22
+ raise WorkflowError, "split-step persistence does not run :#{transition.name}: it is declared from: nil, " \
23
+ "and a prepared step is checked against the state its step is declared from; " \
24
+ "run it with advance_persisted!"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -25,6 +25,7 @@ require_relative "split_step_persistence/state_snapshot"
25
25
  require_relative "split_step_persistence/preparation_claim"
26
26
  require_relative "split_step_persistence/preparation_recovery"
27
27
  require_relative "split_step_persistence/preparation"
28
+ require_relative "split_step_persistence/unsupported_steps"
28
29
  require_relative "split_step_persistence/restart_safe_adapter"
29
30
  require_relative "split_step_persistence/recovery"
30
31
  require_relative "split_step_persistence/recovery_boundary"
@@ -66,6 +67,7 @@ module Smith
66
67
  include PreparationClaim
67
68
  include PreparationRecovery
68
69
  include Preparation
70
+ include UnsupportedSteps
69
71
  include RestartSafeAdapter
70
72
  include Recovery
71
73
  include RecoveryBoundary
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.13.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Ralak
@@ -592,6 +592,7 @@ files:
592
592
  - lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb
593
593
  - lib/smith/workflow/split_step_persistence/transition_contract_signature.rb
594
594
  - lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb
595
+ - lib/smith/workflow/split_step_persistence/unsupported_steps.rb
595
596
  - lib/smith/workflow/step_completion.rb
596
597
  - lib/smith/workflow/step_context.rb
597
598
  - lib/smith/workflow/step_io.rb