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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +1 -1
- data/docs/PERSISTENCE.md +10 -0
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/failure_record_restore.rb +7 -1
- data/lib/smith/workflow/failure_record_text.rb +20 -12
- data/lib/smith/workflow/split_step_persistence/preparation.rb +1 -3
- data/lib/smith/workflow/split_step_persistence/unsupported_steps.rb +29 -0
- data/lib/smith/workflow/split_step_persistence.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c3bc332fe08b2ff077b3039603ddc8356ac65d5034f47abcf4253c3e167f8c6
|
|
4
|
+
data.tar.gz: 9eaf0e8548b473da25838206b63e2a5639d7ee4391f7cb6e30980a5b5aa46e67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
@@ -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:
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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.
|
|
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
|