smith-agents 0.4.4 → 0.5.0
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 +262 -0
- data/README.md +116 -6
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +502 -1
- data/docs/workflow_claim.md +4 -1
- data/lib/smith/agent/lifecycle.rb +19 -10
- data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
- data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
- data/lib/smith/agent/registry.rb +8 -0
- data/lib/smith/agent.rb +24 -2
- data/lib/smith/budget/amount_contract.rb +67 -0
- data/lib/smith/budget/amount_representation.rb +43 -0
- data/lib/smith/budget/decimal_context.rb +18 -0
- data/lib/smith/budget/ledger.rb +110 -18
- data/lib/smith/budget/ledger_publication.rb +60 -0
- data/lib/smith/budget/ledger_state.rb +21 -0
- data/lib/smith/budget/ledger_state_transition.rb +72 -0
- data/lib/smith/budget/public_state_validator.rb +25 -0
- data/lib/smith/budget/reservation.rb +20 -0
- data/lib/smith/budget/reservation_contract.rb +34 -0
- data/lib/smith/context/observation_masking.rb +3 -1
- data/lib/smith/context/state_injection.rb +1 -1
- data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -3
- data/lib/smith/error.rb +5 -0
- data/lib/smith/errors.rb +6 -0
- data/lib/smith/exponential_backoff.rb +128 -0
- data/lib/smith/persistence_adapters/active_record_connection_errors.rb +23 -0
- data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
- data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
- data/lib/smith/persistence_adapters/active_record_initial_write.rb +42 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +95 -52
- data/lib/smith/persistence_adapters/cache_store.rb +4 -1
- data/lib/smith/persistence_adapters/memory.rb +45 -14
- data/lib/smith/persistence_adapters/payload_version.rb +23 -0
- data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
- data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
- data/lib/smith/persistence_adapters/redis_store.rb +38 -43
- data/lib/smith/persistence_adapters/redis_versioned_write.rb +56 -0
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters/version_expectation.rb +19 -0
- data/lib/smith/persistence_adapters.rb +26 -18
- data/lib/smith/persistence_payload_conflict.rb +12 -0
- data/lib/smith/tool/budget_enforcement.rb +2 -2
- data/lib/smith/types.rb +1 -0
- data/lib/smith/version.rb +2 -1
- data/lib/smith/workflow/branch_env.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +21 -11
- data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
- data/lib/smith/workflow/definition_identity.rb +25 -0
- data/lib/smith/workflow/deterministic_step.rb +2 -2
- data/lib/smith/workflow/dsl.rb +73 -5
- data/lib/smith/workflow/durability.rb +2 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
- data/lib/smith/workflow/execution.rb +16 -18
- data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
- data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
- data/lib/smith/workflow/fanout_execution.rb +1 -1
- data/lib/smith/workflow/graph/diagnostic.rb +27 -1
- data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
- data/lib/smith/workflow/graph/execution_successors.rb +65 -0
- data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
- data/lib/smith/workflow/graph/metrics.rb +1 -3
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
- data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
- data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
- data/lib/smith/workflow/graph/reachability.rb +19 -31
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/transition_contract.rb +47 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
- data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
- data/lib/smith/workflow/graph/validator.rb +1 -1
- data/lib/smith/workflow/graph.rb +62 -4
- data/lib/smith/workflow/guardrail_integration.rb +23 -9
- data/lib/smith/workflow/identifier.rb +24 -0
- data/lib/smith/workflow/message_admission.rb +40 -0
- data/lib/smith/workflow/message_admission_boundary.rb +22 -0
- data/lib/smith/workflow/message_batch.rb +60 -0
- data/lib/smith/workflow/message_value_normalizer.rb +133 -0
- data/lib/smith/workflow/nested_execution.rb +10 -0
- data/lib/smith/workflow/orchestrator_worker.rb +3 -7
- data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
- data/lib/smith/workflow/parallel/execution_context.rb +113 -0
- data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
- data/lib/smith/workflow/parallel/root_execution.rb +91 -0
- data/lib/smith/workflow/parallel.rb +19 -22
- data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
- data/lib/smith/workflow/parallel_execution.rb +7 -3
- data/lib/smith/workflow/persistence.rb +49 -10
- data/lib/smith/workflow/prepared_step.rb +122 -0
- data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/retry_execution.rb +20 -11
- data/lib/smith/workflow/split_step_persistence/boundary.rb +128 -0
- data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +119 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +51 -0
- data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +128 -0
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
- data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +22 -0
- data/lib/smith/workflow/split_step_persistence/payloads.rb +54 -0
- data/lib/smith/workflow/split_step_persistence/preparation.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +117 -0
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +54 -0
- data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
- data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
- data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +84 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +102 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb +83 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_signature.rb +107 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb +51 -0
- data/lib/smith/workflow/split_step_persistence.rb +79 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/transition.rb +122 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +57 -19
- data/lib/smith.rb +42 -2
- data/sig/smith.rbs +1 -0
- metadata +131 -19
data/docs/PERSISTENCE.md
CHANGED
|
@@ -120,6 +120,508 @@ workflow.clear_persisted!("ticket:T-1042")
|
|
|
120
120
|
|
|
121
121
|
These helpers do not make Smith a job system or durable runtime. They only remove repetitive restore/checkpoint boilerplate around the configured persistence adapter while leaving queueing, projection, and recovery policy with the host app.
|
|
122
122
|
|
|
123
|
+
### Host-Coordinated Message Admission
|
|
124
|
+
|
|
125
|
+
At a stable workflow boundary, a host can append one message or a bounded batch
|
|
126
|
+
to the persisted session history:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
workflow = ReviewWorkflow.restore("ticket:T-1042", adapter: adapter)
|
|
130
|
+
admission = workflow.append_session_messages!(
|
|
131
|
+
role: :user,
|
|
132
|
+
content: { answer: "The order id is 1042" }
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
workflow.persist!("ticket:T-1042", adapter: adapter)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Smith canonicalizes String and Symbol keys, owns an immutable JSON-like copy,
|
|
139
|
+
appends the batch under the workflow lifecycle mutex, and returns an immutable
|
|
140
|
+
`Smith::Workflow::MessageAdmission`. Its `message_digest` is the SHA-256 digest
|
|
141
|
+
of the canonical message batch and can be correlated with host-owned evidence.
|
|
142
|
+
|
|
143
|
+
Admission is bounded to 100 messages, 64 levels, 100,000 visited values, and one
|
|
144
|
+
MiB of canonical JSON. Integers must fit a signed 64-bit value, Floats must be
|
|
145
|
+
finite, Hash keys must be String or Symbol values, and cyclic or unsupported
|
|
146
|
+
values fail closed. Work is `O(N + sum(K log K))`, where `N` is the number of
|
|
147
|
+
visited values and each `K` is the key count of one Hash; retained output is
|
|
148
|
+
`O(N)`.
|
|
149
|
+
|
|
150
|
+
This API deliberately does not persist, resume, schedule, identify a session,
|
|
151
|
+
or provide idempotency. A durable host must lock its run and checkpoint, verify
|
|
152
|
+
the exact before identity, call admission, persist the next Smith version, and
|
|
153
|
+
record its own cause and before/after evidence atomically. Calling the method
|
|
154
|
+
while ordinary execution or a prepared split-step boundary is active is
|
|
155
|
+
rejected.
|
|
156
|
+
|
|
157
|
+
### Host-Coordinated Step Boundaries
|
|
158
|
+
|
|
159
|
+
Hosts can split one strict transition into explicit prepare, optional exact
|
|
160
|
+
dispatch claim, execution authorization, execute, and checkpoint phases:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
def execute_one_step(workflow, key, adapter)
|
|
164
|
+
transition_name = nil
|
|
165
|
+
|
|
166
|
+
ApplicationRecord.transaction do
|
|
167
|
+
transition_name = workflow.prepare_persisted_step!(key, adapter: adapter)
|
|
168
|
+
next unless transition_name
|
|
169
|
+
|
|
170
|
+
prepared = workflow.prepared_persisted_step
|
|
171
|
+
HostEvent.create!(
|
|
172
|
+
name: "step.started",
|
|
173
|
+
transition: transition_name,
|
|
174
|
+
operation_token: prepared.token,
|
|
175
|
+
preparation_digest: prepared.preparation_digest
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return unless transition_name
|
|
180
|
+
|
|
181
|
+
workflow.confirm_prepared_step!
|
|
182
|
+
|
|
183
|
+
if workflow.class.definition_digest
|
|
184
|
+
dispatch = nil
|
|
185
|
+
ApplicationRecord.transaction do
|
|
186
|
+
dispatch = workflow.claim_prepared_step_dispatch!
|
|
187
|
+
HostAttempt.find_by!(operation_token: workflow.prepared_persisted_step.token)
|
|
188
|
+
.update!(
|
|
189
|
+
status: "dispatch_claimed",
|
|
190
|
+
dispatch_token: dispatch.token,
|
|
191
|
+
dispatch_receipt: dispatch.to_h
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
workflow.confirm_prepared_step_dispatch!
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
authorization = workflow.authorize_prepared_step_execution!
|
|
198
|
+
execution_started = false
|
|
199
|
+
begin
|
|
200
|
+
operation_token = authorization.prepared_step.token
|
|
201
|
+
dispatch_token = authorization.dispatch_claim&.token
|
|
202
|
+
start_status = dispatch_token ? "dispatch_claimed" : "prepared"
|
|
203
|
+
changed = ApplicationRecord.transaction do
|
|
204
|
+
HostAttempt.where(
|
|
205
|
+
operation_token:,
|
|
206
|
+
dispatch_token:,
|
|
207
|
+
status: start_status
|
|
208
|
+
).update_all(status: "executing")
|
|
209
|
+
end
|
|
210
|
+
committed = HostAttempt.find_by(operation_token:, dispatch_token:)
|
|
211
|
+
execution_started = changed == 1 && committed&.status == "executing"
|
|
212
|
+
raise "host execution start did not commit exactly once" unless execution_started
|
|
213
|
+
ensure
|
|
214
|
+
workflow.release_prepared_step_execution!(authorization) unless execution_started
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
execution = workflow.execute_authorized_prepared_step!(authorization) # external work; no host transaction
|
|
218
|
+
raise execution.error if execution.failed?
|
|
219
|
+
|
|
220
|
+
step = execution.step
|
|
221
|
+
|
|
222
|
+
ApplicationRecord.transaction do
|
|
223
|
+
workflow.persist!(key, adapter: adapter)
|
|
224
|
+
HostEvent.create!(name: "step.completed", transition: step.fetch(:transition))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
workflow.complete_persisted_step!
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This contract is available only to workflows using `idempotency_mode :strict`,
|
|
232
|
+
a versioned persistence adapter whose `store_versioned` method accepts `ttl:`,
|
|
233
|
+
and non-expiring workflow persistence. Smith explicitly pins `ttl: nil` for
|
|
234
|
+
every boundary write, so a later global configuration change cannot turn an
|
|
235
|
+
accepted checkpoint into expiring state.
|
|
236
|
+
`prepare_persisted_step!` writes the normal `step_in_progress` marker and returns
|
|
237
|
+
the pending transition name without consuming it. Duplicate preparation on the
|
|
238
|
+
same workflow object is rejected. The boundary is pinned to the exact
|
|
239
|
+
transition object, persistence key, and adapter instance selected during
|
|
240
|
+
preparation. The key is copied into an immutable string and the transition
|
|
241
|
+
contract is frozen. When an adapter can report an open transaction,
|
|
242
|
+
`confirm_prepared_step!` must verify the committed preparation before execution;
|
|
243
|
+
it refuses confirmation while that transaction is still open.
|
|
244
|
+
After preparation, `prepared_persisted_step` returns an immutable
|
|
245
|
+
`Smith::Workflow::PreparedStep` descriptor containing the opaque preparation
|
|
246
|
+
token, transition and origin names, pinned persistence key and logical version,
|
|
247
|
+
next step number, and canonical preparation-payload digest. It exposes no
|
|
248
|
+
workflow context, messages, tool results, prompts, or provider output. The
|
|
249
|
+
descriptor is a host correlation witness for
|
|
250
|
+
the active process-local boundary, not a replacement checkpoint or authority to
|
|
251
|
+
reconstruct and execute a workflow. Its scalar identity remains stable through
|
|
252
|
+
execution and checkpointing. Use `prepared_persisted_step.to_h` when serializing
|
|
253
|
+
the descriptor itself. It returns `nil` before persistence
|
|
254
|
+
acknowledges preparation, after an ambiguous preparation outcome, when
|
|
255
|
+
preparation begins on an already-terminal workflow, and after committed
|
|
256
|
+
completion. With a transactional adapter, the
|
|
257
|
+
descriptor is available inside the still-open transaction so the host can write
|
|
258
|
+
its own correlated record atomically. The adapter's exact transaction identity
|
|
259
|
+
is authoritative even when a host propagates that transaction context across
|
|
260
|
+
fibers or executors. The descriptor is a correlation witness, not proof that the
|
|
261
|
+
transaction subsequently committed.
|
|
262
|
+
Use `Smith::Workflow::PreparedStep.deserialize` to restore a descriptor from a
|
|
263
|
+
Hash or bounded JSON object. The decoder rejects missing, duplicated, and
|
|
264
|
+
unknown attributes, and constrains persistence/step counters to positive signed
|
|
265
|
+
64-bit values; direct `Dry::Struct` construction is not a transport decoder.
|
|
266
|
+
|
|
267
|
+
For a workflow declaring `definition_digest`,
|
|
268
|
+
`claim_prepared_step_dispatch!` atomically replaces the exact `prepared`
|
|
269
|
+
payload with a `dispatching` payload through the adapter's `replace_exact`
|
|
270
|
+
capability. The logical `persistence_version` does not change during this claim.
|
|
271
|
+
Memory uses its monitor, Redis uses WATCH/MULTI/EXEC within the client's native
|
|
272
|
+
no-reconnect scope, and Active Record uses one conditional SQL update while
|
|
273
|
+
advancing its optimistic-lock column. The method
|
|
274
|
+
performs no transition, provider, or tool work,
|
|
275
|
+
so an Active Record host can commit its own attempt-ledger state in the same
|
|
276
|
+
transaction without holding that transaction across external work. After that
|
|
277
|
+
transaction commits, `confirm_prepared_step_dispatch!` verifies the exact
|
|
278
|
+
durable claim. A rolled-back claim restores the known preparation; a missing,
|
|
279
|
+
modified, concurrently claimed, or ambiguously acknowledged payload fails
|
|
280
|
+
closed. `authorize_prepared_step_execution!` then re-verifies the exact durable
|
|
281
|
+
claim and returns one process-local execution capability without performing
|
|
282
|
+
transition, provider, or tool work. A host can commit its own `executing`
|
|
283
|
+
attempt record after authorization and before external work. That host write
|
|
284
|
+
must be a conditional compare-and-set on the exact dispatch identity, and the
|
|
285
|
+
host must re-read committed database state after the outer transaction returns.
|
|
286
|
+
An in-memory record or transaction return value is not commit evidence because
|
|
287
|
+
Rails deliberately swallows `ActiveRecord::Rollback`. If the exact host state
|
|
288
|
+
does not commit, the host must call `release_prepared_step_execution!` with the
|
|
289
|
+
exact capability. `execute_authorized_prepared_step!` consumes it once and
|
|
290
|
+
returns a `PreparedStepExecutionResult`. Its `status`, `failed?`, and `error`
|
|
291
|
+
fields preserve transition failure independently of Smith failure routing; a
|
|
292
|
+
host must not treat a failure-routed workflow state as proof that external work
|
|
293
|
+
succeeded. The result owns an iterative, cycle-aware snapshot bounded to 128
|
|
294
|
+
levels, 100,000 visited values, and 4 MiB of string data. Hashes, Arrays,
|
|
295
|
+
Strings, finite scalar JSON-like values, String or Symbol Hash keys, and the
|
|
296
|
+
exact top-level `StandardError` are supported; other values fail closed. Only the exact-payload
|
|
297
|
+
winner may enter provider, tool, or deterministic step work. Smith snapshots
|
|
298
|
+
and validates a successful step result before advancing workflow state. If the
|
|
299
|
+
external call returns an unsupported or over-limit value, the validation error
|
|
300
|
+
follows the transition's normal failure route and is returned as a typed failed
|
|
301
|
+
result; the success destination is never committed.
|
|
302
|
+
|
|
303
|
+
The authorization rejects copying and the standard Ruby Marshal, Psych/YAML,
|
|
304
|
+
and JSON serialization hooks. It is bound by object identity to one workflow
|
|
305
|
+
instance and by process id to the issuing Ruby process, so a forked child cannot
|
|
306
|
+
consume inherited authority. It is not a lease, cross-process fence, or durable
|
|
307
|
+
attempt receipt.
|
|
308
|
+
Before consuming the capability, a host may inspect the exact captured agent
|
|
309
|
+
bindings with a block:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
authorization.each_agent_binding do |registry_name, agent_class|
|
|
313
|
+
verify_host_execution_identity!(registry_name, agent_class)
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This bounded enumeration performs no provider or tool work and exposes the same
|
|
318
|
+
classes Smith will execute. It is available only while the authorization is
|
|
319
|
+
active in its issuing process. Calling it after release or consumption, from a
|
|
320
|
+
forked process, or without a block fails closed. Smith intentionally does not
|
|
321
|
+
return an `Enumerator`, because deferred iteration could outlive the capability
|
|
322
|
+
scope. Smith first discovers binding names through a bounded graph traversal,
|
|
323
|
+
then resolves the complete bounded set while holding one registry monitor epoch.
|
|
324
|
+
Concurrent registry writes cannot split one authorization across old and new
|
|
325
|
+
registry states. The registry-owned bulk capture reads concrete agent bindings
|
|
326
|
+
without invoking lazy container callbacks, and all public registry mutation
|
|
327
|
+
paths share the same monitor.
|
|
328
|
+
|
|
329
|
+
While that authority is active, Smith's prepended subclass boundary dispatches
|
|
330
|
+
private execution methods through the Smith-owned implementations captured by
|
|
331
|
+
the runtime. This includes nested child workflows that inherit the same
|
|
332
|
+
authorization. Ordinary workflow runs remain polymorphic and continue to honor
|
|
333
|
+
subclass overrides; the membrane exists only for the authorized external-work
|
|
334
|
+
boundary.
|
|
335
|
+
Until the capability is consumed or released, the host must retain exclusive
|
|
336
|
+
authority over that workflow recovery attempt. A process crash after the host
|
|
337
|
+
commits `executing` is an unknown outcome for the host to reconcile; Smith does
|
|
338
|
+
not infer that transition work did or did not begin.
|
|
339
|
+
|
|
340
|
+
`execute_prepared_step!` remains the compatible one-call API. It internally
|
|
341
|
+
authorizes and consumes the step and returns the legacy step Hash. Hosts that
|
|
342
|
+
need a durable pre-dispatch attempt boundary should use the explicit
|
|
343
|
+
authorization API instead.
|
|
344
|
+
|
|
345
|
+
Active Record exact replacement is deliberately a low-level persistence write.
|
|
346
|
+
It does not instantiate the model, run callbacks or validations, or update
|
|
347
|
+
timestamp columns; the payload and locking columns must therefore be treated as
|
|
348
|
+
infrastructure-owned state. Values still use Active Record type casting and
|
|
349
|
+
serialization. This matches Rails' documented
|
|
350
|
+
[`update_all`](https://api.rubyonrails.org/v8.1.1/classes/ActiveRecord/Relation.html#method-i-update_all)
|
|
351
|
+
contract and keeps the comparison and replacement in one SQL statement.
|
|
352
|
+
Smith supplies byte-exact predicates for PostgreSQL, SQLite, MySQL, and Trilogy
|
|
353
|
+
Active Record adapters and fails closed before mutation on an unknown adapter.
|
|
354
|
+
The payload column must be `text` or `string`; JSON/JSONB columns normalize
|
|
355
|
+
representation and therefore cannot satisfy this byte-identity contract.
|
|
356
|
+
|
|
357
|
+
The claim returns an immutable `Smith::Workflow::PreparedStepDispatch` receipt
|
|
358
|
+
containing the prepared-step descriptor, an opaque dispatch token, and the
|
|
359
|
+
canonical dispatch-payload digest. Persist `receipt.to_h` with the host attempt
|
|
360
|
+
record. Use `PreparedStepDispatch.deserialize` for a bounded Hash/JSON transport
|
|
361
|
+
round-trip; direct `Dry::Struct` construction is not a transport decoder.
|
|
362
|
+
|
|
363
|
+
Preparation also takes an O(S) defensive snapshot of
|
|
364
|
+
mutable workflow execution state, where S is the serialized state size. This
|
|
365
|
+
is the necessary ownership boundary: aliases held before preparation cannot
|
|
366
|
+
change what the accepted transition later consumes, and public mutable-state
|
|
367
|
+
readers and `to_state` return defensive snapshots while the boundary is active.
|
|
368
|
+
The canonical correlation digest is computed before dispatch in O(S log K)
|
|
369
|
+
time, where K is the largest object-key count, and is bounded to 4 MiB and
|
|
370
|
+
100,000 JSON nodes. Larger state must use Smith artifact references instead of
|
|
371
|
+
an inline split-step payload.
|
|
372
|
+
Subclass execution entry points remain guarded, and subclass TTL helpers cannot
|
|
373
|
+
override the pinned boundary policy. The
|
|
374
|
+
in-memory marker remains armed so any serialization
|
|
375
|
+
before committed completion still fails closed. Other workflow execution and
|
|
376
|
+
checkpoint APIs are rejected until the host checkpoints the accepted state
|
|
377
|
+
through `persist!`. After that transaction commits,
|
|
378
|
+
`complete_persisted_step!` verifies the exact checkpoint through the adapter
|
|
379
|
+
before clearing the marker and releasing the process-local boundary, and
|
|
380
|
+
likewise refuses to run while the adapter reports an open transaction.
|
|
381
|
+
`prepared_persisted_step?` exposes whether the one execution attempt remains
|
|
382
|
+
available without revealing transition internals.
|
|
383
|
+
|
|
384
|
+
#### Restart-Safe Prepared Recovery
|
|
385
|
+
|
|
386
|
+
Cross-process recovery is opt-in. The workflow class must declare a lowercase
|
|
387
|
+
SHA-256 `definition_digest` covering the complete executable definition:
|
|
388
|
+
workflow topology, deterministic code assets, effective agents, prompts,
|
|
389
|
+
models, tools, schemas, and guardrails. Smith validates this digest but does not
|
|
390
|
+
derive it from Ruby reflection. `Proc#source_location` exposes only a filename
|
|
391
|
+
and line, while `RubyVM::InstructionSequence` is MRI-specific,
|
|
392
|
+
version-sensitive, and not portable across machines or Ruby versions. See the
|
|
393
|
+
official [Proc documentation](https://docs.ruby-lang.org/en/master/Proc.html)
|
|
394
|
+
and [InstructionSequence documentation](https://ruby-doc.org/3.2/RubyVM/InstructionSequence.html).
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
class GeneratedWorkflow < Smith::Workflow
|
|
398
|
+
definition_digest signed_package.digest
|
|
399
|
+
idempotency_mode :strict
|
|
400
|
+
# states and transitions...
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
adapter = Smith::PersistenceAdapters::ActiveRecordStore.new(
|
|
405
|
+
model: WorkflowState,
|
|
406
|
+
identity: "primary:workflow-states"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
descriptor = Smith::Workflow::PreparedStep.deserialize(host_receipt.fetch(:prepared_step))
|
|
410
|
+
decision = Smith::Workflow::PreparedStepRecovery.not_started(descriptor)
|
|
411
|
+
workflow = GeneratedWorkflow.recover_prepared_step(decision, adapter: adapter)
|
|
412
|
+
dispatch = workflow.claim_prepared_step_dispatch!
|
|
413
|
+
authorization = workflow.authorize_prepared_step_execution!
|
|
414
|
+
# Conditionally commit the exact host attempt as executing and re-read it here.
|
|
415
|
+
# Release the authorization unless the committed row proves that this worker
|
|
416
|
+
# won; otherwise consume it outside the transaction.
|
|
417
|
+
execution = workflow.execute_authorized_prepared_step!(authorization)
|
|
418
|
+
|
|
419
|
+
# A replacement worker may continue a committed claim only when the host's
|
|
420
|
+
# exclusive attempt ledger proves transition/provider/tool execution did not start.
|
|
421
|
+
dispatch = Smith::Workflow::PreparedStepDispatch.deserialize(host_receipt.fetch(:dispatch_receipt))
|
|
422
|
+
decision = Smith::Workflow::PreparedStepRecovery.not_started(dispatch)
|
|
423
|
+
workflow = GeneratedWorkflow.recover_prepared_step(decision, adapter: adapter)
|
|
424
|
+
authorization = workflow.authorize_prepared_step_execution!
|
|
425
|
+
execution = workflow.execute_authorized_prepared_step!(authorization)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The host may construct `not_started` only after acquiring exclusive recovery
|
|
429
|
+
authority and proving from its durable attempt ledger that provider/tool
|
|
430
|
+
dispatch never started. Smith does not own leases, queues, attempt records, or
|
|
431
|
+
that fact. Recovery performs one fetch, validates the exact canonical payload,
|
|
432
|
+
class name, schema, definition digest, adapter identity, transition, origin,
|
|
433
|
+
step number, token, key, and version, then reconstructs a guarded process-local
|
|
434
|
+
boundary. It refuses an open adapter transaction and never migrates an
|
|
435
|
+
in-progress payload. Ordinary `restore` remains fail-closed.
|
|
436
|
+
|
|
437
|
+
Restart-safe preparation additionally requires a non-expiring adapter exposing
|
|
438
|
+
both `replace_exact` and a stable, bounded `persistence_identity`. Configure the
|
|
439
|
+
same non-secret identity for every process connected to one durable storage
|
|
440
|
+
domain. Memory's generated identity is process-local and is suitable only for
|
|
441
|
+
same-process tests. Cache adapters do not provide exact CAS and are unsupported
|
|
442
|
+
for restart-safe prepared recovery.
|
|
443
|
+
|
|
444
|
+
Recovery authorizes a payload already marked `dispatching` only when the host
|
|
445
|
+
supplies the exact committed `PreparedStepDispatch` receipt and an explicit
|
|
446
|
+
`not_started` decision. The host may make that decision only while holding
|
|
447
|
+
exclusive recovery authority and after its durable attempt ledger proves that
|
|
448
|
+
transition/provider/tool execution never started. Without that proof, a crash
|
|
449
|
+
or lost acknowledgement after the exact claim has an uncertain external
|
|
450
|
+
outcome. Smith never rewinds or executes it on its own.
|
|
451
|
+
|
|
452
|
+
When claim and host-attempt writes share an Active Record transaction,
|
|
453
|
+
`claim_prepared_step_dispatch!` leaves execution unavailable until
|
|
454
|
+
`confirm_prepared_step_dispatch!` runs after commit. A rollback restores the
|
|
455
|
+
known prepared boundary and permits a new exact claim. Any other confirmation
|
|
456
|
+
result remains uncertain and cannot execute.
|
|
457
|
+
|
|
458
|
+
The definition digest is pinned and sealed on the class object when preparation
|
|
459
|
+
or recovery authority is acquired. A concurrent DSL setter either completes
|
|
460
|
+
before sealing and becomes the pinned identity, or raises before claim and
|
|
461
|
+
execution. Repeating the same digest remains idempotent. A reloaded definition
|
|
462
|
+
must use a new class object; recovery through that class still validates its
|
|
463
|
+
digest against the durable descriptor.
|
|
464
|
+
|
|
465
|
+
Exact replacement is a current-value compare-and-swap, not a historical fencing
|
|
466
|
+
ledger. A restart-safe workflow key must be exclusively owned by Smith's
|
|
467
|
+
versioned/exact persistence path. Hosts must not call unconditional `store`,
|
|
468
|
+
delete/recreate the key, or restore an earlier byte-identical payload while a
|
|
469
|
+
boundary is active. Such an out-of-band A-to-B-to-A rewrite is
|
|
470
|
+
information-theoretically indistinguishable from the original value without a
|
|
471
|
+
separate monotonic storage generation. Hosts that permit other writers must
|
|
472
|
+
fence them in their own durable attempt/storage ledger; Smith does not absorb
|
|
473
|
+
that host policy.
|
|
474
|
+
|
|
475
|
+
Redis versioned and exact CAS require a client-native reconnect-disabling
|
|
476
|
+
scope around WATCH/MULTI/EXEC. redis-rb 5.4 exposes `without_reconnect`, while
|
|
477
|
+
newer clients may expose `disable_reconnection`; Smith accepts either and
|
|
478
|
+
refuses CAS before WATCH when neither exists. This follows redis-rb's official
|
|
479
|
+
[reconnection contract](https://github.com/redis/redis-rb#reconnections) and
|
|
480
|
+
prevents a transaction from being replayed after connection-scoped WATCH state
|
|
481
|
+
has been lost.
|
|
482
|
+
|
|
483
|
+
Workflow classes are part of the execution contract once preparation begins.
|
|
484
|
+
Hosts must not add or prepend methods to that class until the boundary is
|
|
485
|
+
complete, and custom `inherited` hooks must call `super`. The same rule applies
|
|
486
|
+
to reachable nested workflow and captured agent classes. Smith captures and
|
|
487
|
+
revalidates nested transition contracts and exact agent class bindings, while a
|
|
488
|
+
host must not concurrently mutate those class objects. Ruby intentionally
|
|
489
|
+
allows open-class mutation. Smith blocks definition-digest changes after the
|
|
490
|
+
class is sealed, while other post-preparation class mutation remains an
|
|
491
|
+
unsupported host lifecycle violation rather than something Smith attempts to
|
|
492
|
+
sandbox.
|
|
493
|
+
|
|
494
|
+
The lifecycle row and Smith checkpoint are atomic only when the persistence
|
|
495
|
+
adapter participates in the same transaction and database connection domain as
|
|
496
|
+
the host record. `ActiveRecordStore` can provide that property when its model
|
|
497
|
+
uses the same database connection; it also reports transaction state so Smith
|
|
498
|
+
cannot confirm an uncommitted boundary. Memory, Redis, cache-backed,
|
|
499
|
+
cross-database, and external adapters do not participate in
|
|
500
|
+
`ApplicationRecord.transaction`; hosts using them must supply their own
|
|
501
|
+
coordination protocol and must not claim the two writes are atomic.
|
|
502
|
+
Custom transactional adapters should implement both `transaction_open?` and
|
|
503
|
+
`transaction_identity`. The identity must be stable for one open transaction or
|
|
504
|
+
savepoint and change for every later transaction. Smith's `ActiveRecordStore`
|
|
505
|
+
uses the public `current_transaction.uuid` API for this exact fence. Smith fails
|
|
506
|
+
before writing when an open transaction has no exact identity capability.
|
|
507
|
+
|
|
508
|
+
If execution raises or the process dies after external work, the durable marker
|
|
509
|
+
remains set. A strict restore therefore fails closed with
|
|
510
|
+
`StepInProgressOnRestore`; the host must reconcile operation results or classify
|
|
511
|
+
the run as uncertain rather than blindly replaying the transition. The same
|
|
512
|
+
in-memory workflow object cannot retry an attempted transition. If a host
|
|
513
|
+
transaction rolls back after a successful `persist!`,
|
|
514
|
+
`complete_persisted_step!` rejects the rolled-back checkpoint. When the adapter
|
|
515
|
+
still contains the exact committed preparation, Smith restores that known
|
|
516
|
+
version and permits the same in-memory object to retry the unchanged checkpoint.
|
|
517
|
+
Any different or missing payload remains uncertain and non-retryable.
|
|
518
|
+
|
|
519
|
+
Ambiguous persistence acknowledgements also fail closed. If preparation may
|
|
520
|
+
have written before raising, the object cannot execute. If a post-step
|
|
521
|
+
checkpoint may have written before raising, `complete_persisted_step!` can
|
|
522
|
+
verify the exact attempted payload without replaying the write. The host must
|
|
523
|
+
perform that reconciliation before retrying. If the adapter still contains the
|
|
524
|
+
exact preparation, Smith permits one unchanged retry; any other result remains
|
|
525
|
+
non-retryable. Smith retains one checkpoint witness, so retry bookkeeping is
|
|
526
|
+
constant space rather than growing with the number of attempts.
|
|
527
|
+
|
|
528
|
+
Transition contract capture uses cycle-aware `O(V + E)` traversal, with time
|
|
529
|
+
and space bounded to 10,000 visits, 4 MiB of string data, and a maximum depth
|
|
530
|
+
of 128. Strings are represented by SHA-256 digests rather than duplicated.
|
|
531
|
+
Supported mutable data shapes are frozen, `Range` endpoints are traversed, and
|
|
532
|
+
opaque mutable objects fail closed instead of receiving a misleading shallow
|
|
533
|
+
signature. Callable identity is pinned for the boundary; Ruby closure state and
|
|
534
|
+
method bodies remain host lifecycle concerns. Smith detects contract
|
|
535
|
+
replacement but does not claim to sandbox Ruby's open object model. Subclass
|
|
536
|
+
entry points remain guarded even when a host prepends modules after class
|
|
537
|
+
creation, and prepared execution bypasses host `advance!` wrappers so they
|
|
538
|
+
cannot become transition execution authority.
|
|
539
|
+
|
|
540
|
+
## Active Record Optimistic Locking
|
|
541
|
+
|
|
542
|
+
`ActiveRecordStore` keeps two version domains deliberately separate:
|
|
543
|
+
|
|
544
|
+
- `persistence_version` inside the JSON payload is Smith's logical workflow
|
|
545
|
+
version and is compared with `expected_version`.
|
|
546
|
+
- the host model's Active Record `locking_column` is Rails' row-level
|
|
547
|
+
compare-and-swap token and detects concurrent updates between load and save.
|
|
548
|
+
|
|
549
|
+
Across every versioned adapter, a missing key can be created only with
|
|
550
|
+
`expected_version: 0`. A nonzero expected version against missing state raises
|
|
551
|
+
`PersistenceVersionConflict` with `actual: :missing`, preventing stale workflow
|
|
552
|
+
objects from resurrecting host-deleted state.
|
|
553
|
+
|
|
554
|
+
Legacy object-shaped payloads without `persistence_version` use version zero.
|
|
555
|
+
Valid JSON scalars are not workflow-state documents, and explicit null,
|
|
556
|
+
negative, or non-integer versions fail closed instead of being overwritten.
|
|
557
|
+
|
|
558
|
+
The default host model needs a unique key, a JSON/text payload, and Rails'
|
|
559
|
+
standard optimistic-locking column:
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
create_table :workflow_states do |t|
|
|
563
|
+
t.string :key, null: false, index: { unique: true }
|
|
564
|
+
t.text :payload, null: false
|
|
565
|
+
t.integer :lock_version, null: false, default: 0
|
|
566
|
+
t.timestamps
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
class WorkflowState < ApplicationRecord
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
Smith.configure do |config|
|
|
573
|
+
config.persistence_adapter = :active_record
|
|
574
|
+
config.persistence_options = {
|
|
575
|
+
model: WorkflowState,
|
|
576
|
+
identity: "primary:workflow-states"
|
|
577
|
+
}
|
|
578
|
+
end
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
For a custom locking column, configure both the host model and adapter. Smith
|
|
582
|
+
validates this relationship but never mutates the host model:
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
add_column :workflow_states, :workflow_revision, :integer,
|
|
586
|
+
null: false,
|
|
587
|
+
default: 0
|
|
588
|
+
|
|
589
|
+
class WorkflowState < ApplicationRecord
|
|
590
|
+
self.locking_column = :workflow_revision
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
Smith.configure do |config|
|
|
594
|
+
config.persistence_adapter = :active_record
|
|
595
|
+
config.persistence_options = {
|
|
596
|
+
model: WorkflowState,
|
|
597
|
+
version_column: :workflow_revision
|
|
598
|
+
}
|
|
599
|
+
end
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The unique key must be enforced by the database. Before every exact write,
|
|
603
|
+
Smith verifies from database metadata that the configured key is the database
|
|
604
|
+
primary key or has its own unconditional single-column unique index. Model-only
|
|
605
|
+
primary-key declarations, partial indexes, and composite indexes are not
|
|
606
|
+
accepted. Smith uses Active Record's table-keyed schema cache; a cold check is
|
|
607
|
+
`O(I)` in the table's index count and never scans workflow rows. Initial
|
|
608
|
+
creation uses Rails' native
|
|
609
|
+
`create_or_find_by!` savepoint path, so a concurrent key insert does not
|
|
610
|
+
invalidate a PostgreSQL caller transaction. A collision on another unique
|
|
611
|
+
constraint is not misreported as a key-version conflict.
|
|
612
|
+
|
|
613
|
+
Versioned writes are single-attempt. Smith translates known connection failures
|
|
614
|
+
to `PersistenceIOError`, but never replays an uncertain write below the
|
|
615
|
+
host-owned transaction boundary. The host must restore and reconcile persisted
|
|
616
|
+
state before deciding whether to retry. Keep the host model persistence-focused:
|
|
617
|
+
callbacks that raise after commit run after the write is permanent and therefore
|
|
618
|
+
create an inherently uncertain outcome.
|
|
619
|
+
|
|
620
|
+
The adapter participates in the caller's Active Record transaction and never
|
|
621
|
+
commits it. Hosts remain responsible for outer transaction boundaries, queue
|
|
622
|
+
claims, fencing, reconciliation, and recovery policy. See the
|
|
623
|
+
[official Rails optimistic-locking documentation](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html).
|
|
624
|
+
|
|
123
625
|
## Artifacts
|
|
124
626
|
|
|
125
627
|
Use artifacts when outputs are too large to keep inline.
|
|
@@ -166,4 +668,3 @@ Why this matters:
|
|
|
166
668
|
- large payloads can move out of the inline workflow result
|
|
167
669
|
- refs are execution-scoped
|
|
168
670
|
- nested workflows inherit artifact scope correctly
|
|
169
|
-
|
data/docs/workflow_claim.md
CHANGED
|
@@ -55,4 +55,7 @@ If the model has AASM and you want to skip events, call `.cas` explicitly; if yo
|
|
|
55
55
|
|
|
56
56
|
## Testing
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
The default spec suite boots an in-memory SQLite database and runs the tagged
|
|
59
|
+
Active Record strategy examples against the `ClaimableRecord` fixture model.
|
|
60
|
+
Package smoke tests separately verify that requiring Smith does not make Active
|
|
61
|
+
Record a runtime dependency.
|
|
@@ -21,9 +21,9 @@ module Smith
|
|
|
21
21
|
instance.after_completion(result, context)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def invoke_agent(agent_class, prepared_input)
|
|
24
|
+
def invoke_agent(agent_class, prepared_input, output_schema: agent_class.output_schema)
|
|
25
25
|
check_deadline!
|
|
26
|
-
response, model_used = complete_with_provider(agent_class, prepared_input)
|
|
26
|
+
response, model_used = complete_with_provider(agent_class, prepared_input, output_schema:)
|
|
27
27
|
snapshot_and_finalize(agent_class, response, model_used)
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -33,12 +33,12 @@ module Smith
|
|
|
33
33
|
# parallel fan-out, two branches sharing the workflow could race
|
|
34
34
|
# and attribute the wrong model to the wrong response. Local data
|
|
35
35
|
# eliminates the race entirely.
|
|
36
|
-
def complete_with_provider(agent_class, prepared_input)
|
|
36
|
+
def complete_with_provider(agent_class, prepared_input, output_schema:)
|
|
37
37
|
models = build_model_chain(agent_class)
|
|
38
38
|
|
|
39
39
|
models.each_with_index do |model_id, index|
|
|
40
40
|
check_deadline! if index.positive?
|
|
41
|
-
response = attempt_model(agent_class, prepared_input, model_id)
|
|
41
|
+
response = attempt_model(agent_class, prepared_input, model_id, output_schema:)
|
|
42
42
|
return [response, model_id]
|
|
43
43
|
rescue Smith::Error
|
|
44
44
|
raise
|
|
@@ -71,10 +71,10 @@ module Smith
|
|
|
71
71
|
"model block for #{agent_class} must return a non-empty string; got #{result.inspect}"
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def attempt_model(agent_class, prepared_input, model_id)
|
|
74
|
+
def attempt_model(agent_class, prepared_input, model_id, output_schema:)
|
|
75
75
|
chat = agent_class.chat(model: model_id, **bridge_workflow_inputs(agent_class))
|
|
76
76
|
add_prepared_input(chat, prepared_input)
|
|
77
|
-
chat = chat.with_schema(
|
|
77
|
+
chat = chat.with_schema(output_schema) if output_schema
|
|
78
78
|
chat.complete
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -121,7 +121,7 @@ module Smith
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
merge_system_messages!(chat, system_messages) if system_messages.any?
|
|
124
|
-
other_messages.each { |message|
|
|
124
|
+
other_messages.each { |message| add_message(chat, message) }
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def provider_safe_prepared_input(prepared_input)
|
|
@@ -141,7 +141,7 @@ module Smith
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def merge_system_messages!(chat, prepared_system_messages)
|
|
144
|
-
return prepared_system_messages.each { |message|
|
|
144
|
+
return prepared_system_messages.each { |message| add_message(chat, message) } unless chat.respond_to?(:messages)
|
|
145
145
|
|
|
146
146
|
existing_system_contents = chat.messages.filter_map do |message|
|
|
147
147
|
message.content if message_role(message) == :system
|
|
@@ -153,16 +153,25 @@ module Smith
|
|
|
153
153
|
combined_contents = existing_system_contents + prepared_system_contents
|
|
154
154
|
return if combined_contents.empty?
|
|
155
155
|
unless combined_contents.all?(String)
|
|
156
|
-
return prepared_system_messages.each { |message|
|
|
156
|
+
return prepared_system_messages.each { |message| add_message(chat, message) }
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
if chat.respond_to?(:with_instructions)
|
|
160
160
|
chat.with_instructions(combined_contents.join("\n\n"))
|
|
161
161
|
else
|
|
162
|
-
prepared_system_messages.each { |message|
|
|
162
|
+
prepared_system_messages.each { |message| add_message(chat, message) }
|
|
163
163
|
end
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
def add_message(chat, message)
|
|
167
|
+
attributes = if message.is_a?(Hash)
|
|
168
|
+
message.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
|
|
169
|
+
else
|
|
170
|
+
message
|
|
171
|
+
end
|
|
172
|
+
chat.add_message(attributes)
|
|
173
|
+
end
|
|
174
|
+
|
|
166
175
|
def message_role(message)
|
|
167
176
|
message_attribute(message, :role)&.to_sym
|
|
168
177
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Agent
|
|
5
|
+
module Registry
|
|
6
|
+
module ExecutionBindingCapture
|
|
7
|
+
def capture_bindings!(requests)
|
|
8
|
+
registry_monitor.synchronize do
|
|
9
|
+
requests.each_with_object({}) do |request, bindings|
|
|
10
|
+
key = normalize_key(request.fetch(:name))
|
|
11
|
+
binding = binding_for(key)
|
|
12
|
+
klass = binding&.fetch(:agent_class, nil)
|
|
13
|
+
unless klass
|
|
14
|
+
raise Smith::WorkflowError,
|
|
15
|
+
"unresolved #{request.fetch(:role)} :#{key}" \
|
|
16
|
+
"#{fetch_suffix(request.fetch(:workflow_class), request.fetch(:transition_name))}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
bindings[key] = klass
|
|
20
|
+
end.freeze
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Agent
|
|
5
|
+
module Registry
|
|
6
|
+
module MutationBoundary
|
|
7
|
+
def merge(...)
|
|
8
|
+
registry_monitor.synchronize { super }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def decorate(...)
|
|
12
|
+
registry_monitor.synchronize { super }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def namespace(...)
|
|
16
|
+
registry_monitor.synchronize { super }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def import(...)
|
|
20
|
+
registry_monitor.synchronize { super }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure(...)
|
|
24
|
+
registry_monitor.synchronize { super }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|