phronomy 0.28.0 → 0.30.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 +96 -1
- data/README.md +32 -6
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
- data/docs/architecture/agent-iteration-budget.md +81 -0
- data/docs/architecture/dx05-agent-scope.md +126 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
- data/docs/architecture/generator-verifier-ownership.md +18 -109
- data/docs/architecture/llm-continuation-metadata.md +88 -0
- data/docs/architecture/lm-studio-output-failures.md +111 -0
- data/docs/architecture/r8-unit15.md +3 -0
- data/docs/architecture/r8-unit7.md +2 -1
- data/docs/architecture/structured-output-and-scope.md +183 -0
- data/docs/architecture/tracing.md +22 -4
- data/docs/architecture/transport-error-boundaries.md +103 -0
- data/docs/architecture/trust-acceptance-followup.md +87 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +14 -2
- data/docs/getting-started.md +44 -43
- data/docs/migrations/r8-unit15.md +3 -0
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +10 -0
- data/lib/phronomy/agent/admission.rb +1 -1
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +121 -88
- data/lib/phronomy/agent/base.rb +82 -44
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/context_importer.rb +4 -0
- data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
- data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
- data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
- data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
- data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
- data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
- data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
- data/lib/phronomy/agent/execution_observation.rb +5 -5
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/output_contract.rb +64 -0
- data/lib/phronomy/agent/output_error.rb +53 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
- data/lib/phronomy/agent/runtime_input.rb +15 -4
- data/lib/phronomy/agent/saved_context_reader.rb +2 -1
- data/lib/phronomy/agent/store.rb +10 -2
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/common/values/immutable.rb +33 -0
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +18 -22
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/engine/fsm_session.rb +16 -1
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
- data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
- data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
- data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
- data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
- data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
- data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
- data/lib/phronomy/generation/generator_verifier.rb +170 -85
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
- data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
- data/lib/phronomy/llm_adapter/message.rb +4 -0
- data/lib/phronomy/llm_adapter/request.rb +17 -2
- data/lib/phronomy/llm_adapter/response.rb +11 -4
- data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/shared_state.rb +1 -0
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
- data/lib/phronomy/output_parser/parse_error.rb +10 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/automatic.rb +14 -8
- data/lib/phronomy/tracing/base.rb +37 -7
- data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
- data/lib/phronomy/tracing/observation.rb +2 -1
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +7 -2
- data/scripts/api_snapshot.rb +15 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/scripts/migrate_generator_verifier_result.rb +18 -0
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/execution_contract.rbs +4 -3
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +36 -5
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +8 -2
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/generator_verifier.rbs +70 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
- data/sig/phronomy/llm_adapter/values.rbs +8 -2
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/tracing/observation.rbs +1 -1
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +6 -0
- metadata +103 -5
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
- data/lib/phronomy/generation/low_confidence_error.rb +0 -14
- data/lib/phronomy/runtime_composition/generation_defaults.rb +0 -6
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# RubyLLM transport error boundaries
|
|
2
|
+
|
|
3
|
+
## Purpose and ownership
|
|
4
|
+
|
|
5
|
+
Applications need to handle a failed provider communication through Phronomy's
|
|
6
|
+
public API without knowing the HTTP client hidden behind the selected backend.
|
|
7
|
+
LLM generation and embedding are separate contracts, so each retains its own
|
|
8
|
+
existing `TransportError`. The concrete RubyLLM backends translate identifiable
|
|
9
|
+
communication failures; Agent, Generation and Execution propagate that result.
|
|
10
|
+
|
|
11
|
+
This change adds no public class, parameter, failure code, cross-contract edge,
|
|
12
|
+
shared error translator or retry mechanism. Faraday references remain in the two
|
|
13
|
+
concrete backends. The public RBS shape is unchanged.
|
|
14
|
+
|
|
15
|
+
## Mapping and exclusions
|
|
16
|
+
|
|
17
|
+
| Failure at the RubyLLM boundary | LLM complete/stream | Embeddings |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `Faraday::TimeoutError` | `LLMAdapter::TransportError` | `Embeddings::TransportError` |
|
|
20
|
+
| `Faraday::ConnectionFailed` | `LLMAdapter::TransportError` | `Embeddings::TransportError` |
|
|
21
|
+
| Existing RubyLLM errors | Existing authentication/rate-limit/context/transport classifications | Existing authentication/rate-limit/transport classifications |
|
|
22
|
+
| Application streaming callback, including either Faraday class above | Original exception identity | Not applicable |
|
|
23
|
+
| Cancellation, execution deadline, arbitrary I/O, programming failure or unrecognized Faraday error | Original exception identity | Original exception identity |
|
|
24
|
+
|
|
25
|
+
The top-level mapped exception preserves the original message and its SDK/HTTP
|
|
26
|
+
exception as `cause`. That exception's network cause is retained, for example:
|
|
27
|
+
`LLMAdapter::TransportError -> Faraday::TimeoutError -> Net::ReadTimeout`.
|
|
28
|
+
No classification is inferred from an exception's message or nested cause.
|
|
29
|
+
The explicitly selected LM Studio backend additionally decodes the HTTP response
|
|
30
|
+
body at the SDK boundary using a narrow provider-response mapping; see
|
|
31
|
+
[LM Studio output failures](lm-studio-output-failures.md). This is not a generic
|
|
32
|
+
exception-message or cause-chain heuristic, and the default RubyLLM mapping below
|
|
33
|
+
is unchanged.
|
|
34
|
+
Generic `Faraday::Error`, `Faraday::SSLError`, raw `IOError` and system errors are
|
|
35
|
+
not added to this mapping. Future additions require evidence and an explicit
|
|
36
|
+
category decision rather than a blanket rescue.
|
|
37
|
+
|
|
38
|
+
The LLM backend's callback identity check runs before provider classification.
|
|
39
|
+
An application callback throwing `Faraday::TimeoutError` therefore remains the
|
|
40
|
+
same object. A provider timeout after a delivered chunk is a transport failure;
|
|
41
|
+
the already delivered chunk is not withdrawn, replayed or turned into success.
|
|
42
|
+
|
|
43
|
+
## Application code before and after
|
|
44
|
+
|
|
45
|
+
Before, handling a provider timeout required knowledge of the backend's client:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
begin
|
|
49
|
+
response = adapter.complete(request)
|
|
50
|
+
rescue Phronomy::LLMAdapter::TransportError,
|
|
51
|
+
Faraday::TimeoutError, Faraday::ConnectionFailed => error
|
|
52
|
+
warn "Provider communication failed: #{error.message}"
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After, the same communication categories are handled through the public owner:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
begin
|
|
60
|
+
response = adapter.complete(request)
|
|
61
|
+
rescue Phronomy::LLMAdapter::TransportError => error
|
|
62
|
+
warn "Provider communication failed: #{error.message}"
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Embeddings applications use `Phronomy::Embeddings::TransportError` in the same
|
|
67
|
+
way. Applications may inspect `cause` for diagnostics, but ordinary transport
|
|
68
|
+
handling does not need a Faraday dependency. Existing specialized errors remain
|
|
69
|
+
available for applications that distinguish authentication or rate limiting.
|
|
70
|
+
|
|
71
|
+
## Retry, interruption and lifetime
|
|
72
|
+
|
|
73
|
+
The accepted [transport-policy ADR](../decisions/011-delegate-transport-policy-to-adapters.md)
|
|
74
|
+
continues to apply. RubyLLM owns its configured HTTP retries. Phronomy translates
|
|
75
|
+
the failure reaching its boundary and does not replay the LLM operation, Agent
|
|
76
|
+
invocation or Tool work. A transport classification does not assert that no
|
|
77
|
+
output or external effect occurred, and does not authorize a retry.
|
|
78
|
+
|
|
79
|
+
An execution deadline and an HTTP timeout remain different failures. This change
|
|
80
|
+
does not force worker threads to stop, alter cancellation, or make an active
|
|
81
|
+
Agent safe to unload. Once an ordinary provider failure has settled and its
|
|
82
|
+
physical work has finished, the existing lifetime APIs can release its owners.
|
|
83
|
+
|
|
84
|
+
The old Faraday 2.1.0 / faraday-net_http 2.1.0 streaming path lost HTTP status and
|
|
85
|
+
caused RubyLLM to retry a schema/Tool rejection as a synthetic ServerError.
|
|
86
|
+
The supported floors are now Faraday 2.14.4 / faraday-net_http 3.4.4. Real HTTP
|
|
87
|
+
tests verify the original status and request count with SDK retries enabled.
|
|
88
|
+
The obsolete ServerError-to-schema-rejection fallback is removed: an actual 500
|
|
89
|
+
remains a server failure even when its text resembles a known 400 rejection.
|
|
90
|
+
Configured SDK retries for 429/500 remain intact; no global retry switch or
|
|
91
|
+
SDK monkey patch is introduced.
|
|
92
|
+
|
|
93
|
+
## Verification
|
|
94
|
+
|
|
95
|
+
Unit contracts cover both mapped types across complete, stream and embed,
|
|
96
|
+
identity exclusions, same-class callback failures and failure after partial
|
|
97
|
+
output. Loopback HTTP tests exercise the real RubyLLM/Faraday stack for timeout,
|
|
98
|
+
connection loss, partial streaming with SDK retries enabled, callback identity,
|
|
99
|
+
and GeneratorVerifier propagation/release. The minimum HTTP CI job runs these
|
|
100
|
+
contracts too. Test-only shortened timeouts do not change production settings.
|
|
101
|
+
|
|
102
|
+
These tests do not certify live model answer quality, schema/Tool combination
|
|
103
|
+
support, or the root cause of the earlier unreproduced LM Studio wait.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Trust pipeline acceptance follow-up
|
|
2
|
+
|
|
3
|
+
## Purpose and responsibility
|
|
4
|
+
|
|
5
|
+
Communication integrity, schema validity and business truth are different
|
|
6
|
+
promises. Successful HTTP and valid JSON cannot establish faithful extraction.
|
|
7
|
+
|
|
8
|
+
| Concern | Owner | Change |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| HTTP status and configured retries | HTTP stack / concrete RubyLLM backend | Require a verified stack that preserves streaming status |
|
|
11
|
+
| Schema-valid terminal values | Agent, using OutputParser | Existing guarantee retained |
|
|
12
|
+
| Evaluation criteria and result meaning | GeneratorVerifier | Existing protocol retained |
|
|
13
|
+
| Exact invoice extraction | Application / acceptance oracle | Reject incorrect values explicitly |
|
|
14
|
+
| Approval decision and Tool effects | Application decision, Agent execution, Generation routing | Observe notification, decision, effects and release |
|
|
15
|
+
| Unexplained wait | Diagnostic runner plus server logs | Record payload, settings, attempts and periodic thread stacks |
|
|
16
|
+
|
|
17
|
+
Contract owners, their dependencies, Engine and Execution are unchanged.
|
|
18
|
+
No SDK monkey patch, extra retry loop, output repair or schema downgrade is added.
|
|
19
|
+
|
|
20
|
+
## HTTP dependency correction
|
|
21
|
+
|
|
22
|
+
Faraday 2.1.0 with faraday-net_http 2.1.0 did not pass the response environment to
|
|
23
|
+
streaming callbacks. RubyLLM 2.0.0 could interpret a JSON 400 as a synthetic 500.
|
|
24
|
+
401 and 429 could also lose their identity. The ServerError fallback additionally
|
|
25
|
+
misclassified an actual 500 whose text matched the known schema rejection.
|
|
26
|
+
|
|
27
|
+
Supported floors are now Faraday 2.14.4 and faraday-net_http 3.4.4, the pair
|
|
28
|
+
already verified on the normal path. Both are explicit in the gemspec: raising
|
|
29
|
+
Faraday alone does not exclude older Net::HTTP adapters. CI pins and checks both,
|
|
30
|
+
with the existing minimum RubyLLM, MCP and JSON versions. These are verified
|
|
31
|
+
support floors, not claims about the earliest upstream fix. Custom Faraday
|
|
32
|
+
adapters need their own streaming/status compatibility assessment.
|
|
33
|
+
|
|
34
|
+
The obsolete ServerError fallback is removed. Only a real BadRequestError with
|
|
35
|
+
the known body becomes StructuredOutputUnsupportedError. Actual 500 responses
|
|
36
|
+
retain the SDK server-error cause. Configured SDK retries for 429/500 remain;
|
|
37
|
+
a 400 rejection is one HTTP attempt even with three retries configured.
|
|
38
|
+
Partial-stream replay and callback identity regressions remain in place.
|
|
39
|
+
|
|
40
|
+
Before and after, application handling uses the same public API:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
rescue Phronomy::LLMAdapter::StructuredOutputUnsupportedError => error
|
|
44
|
+
warn error.cause.class
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Before, the old stack could issue four requests and expose a synthetic ServerError
|
|
48
|
+
for HTTP 400. After, the cause is BadRequestError and that rejection is not
|
|
49
|
+
retried as a transient server failure.
|
|
50
|
+
|
|
51
|
+
## Content fidelity and acceptance
|
|
52
|
+
|
|
53
|
+
The invoice demo keeps its general string/number schema and asks the model to
|
|
54
|
+
preserve identifier characters. After Agent release, an independent oracle checks
|
|
55
|
+
the fixed fixture. Incorrect values exit 1; they are never replaced or retried.
|
|
56
|
+
This is fixture acceptance, not a general invoice validator or Agent responsibility.
|
|
57
|
+
|
|
58
|
+
Before, INV-2026-01 could print and exit successfully for source INV-2026-001.
|
|
59
|
+
After, it is a fidelity failure, while schema validation remains correctly
|
|
60
|
+
successful. One correct response does not establish general model accuracy.
|
|
61
|
+
|
|
62
|
+
The examples runner checks invoice, accepted, iteration_limit and approval
|
|
63
|
+
yes/no/cancel independently. Approval requires a notification and the expected
|
|
64
|
+
Tool effects. A schema/Tool rejection is blocked, not successful approval.
|
|
65
|
+
Yes checks approval and valid completion; accepted is a separate test because
|
|
66
|
+
Tool approval does not guarantee that evaluation criteria will be satisfied.
|
|
67
|
+
|
|
68
|
+
## Wait diagnosis and limits
|
|
69
|
+
|
|
70
|
+
The opt-in runner observes the pinned SDK without replacing the provider or
|
|
71
|
+
changing timeout/retry settings. It records UTC times, request payloads/hashes,
|
|
72
|
+
settings, physical HTTP attempts, results, exception causes, releases and periodic
|
|
73
|
+
Ruby thread stacks. Observer failures invalidate the report and do not replace
|
|
74
|
+
the application exception. Signals wrapped by cleanup errors remain interrupted.
|
|
75
|
+
|
|
76
|
+
Observation uses diagnostic-only SDK method bindings and must be reviewed when
|
|
77
|
+
the SDK changes. Reports contain prompts/responses. Sampling and file writes add
|
|
78
|
+
overhead; this runner is not a performance benchmark.
|
|
79
|
+
|
|
80
|
+
Historical LM Studio logs cannot be reconstructed. Match the saved review payload
|
|
81
|
+
and UTC times with server logs; use future wait stacks to identify pending
|
|
82
|
+
HTTP/SDK/framework operations before assigning a cause. A later fast response
|
|
83
|
+
does not resolve the earlier wait.
|
|
84
|
+
|
|
85
|
+
Controlled HTTP fixtures prove framework and observation behavior, not live
|
|
86
|
+
model accuracy or server support for schema plus Tools. Live acceptance still
|
|
87
|
+
requires a suitable model/server environment and recorded server version.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Failure reason codes
|
|
2
|
+
|
|
3
|
+
`Phronomy::Error` and its subclasses accept an optional `code:` String and expose
|
|
4
|
+
it through `error.code`. Unspecified codes are nil. Supplied strings are copied
|
|
5
|
+
and frozen. Existing message, class, cause and backtrace behavior is retained.
|
|
6
|
+
No change is made to StandardError or third-party exceptions.
|
|
7
|
+
|
|
8
|
+
This is an additive public-contract extension. `class` and `message` remain in
|
|
9
|
+
saved failure diagnostics. When a Phronomy exception has a code, its diagnostic
|
|
10
|
+
also contains `"code"`. The same generic field is retained through Agent failure
|
|
11
|
+
persistence, Team results, and terminal resume/reboot. A code is not an execution
|
|
12
|
+
status and is not a retry permission.
|
|
13
|
+
|
|
14
|
+
| Code | Owner and meaning |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `team.enqueue_after_finalize` | MultiAgent: a new enqueue was conclusively rejected after finalization, following the existing readback checks |
|
|
17
|
+
|
|
18
|
+
The operation wrapper remains catchable as ConfigurationError. A Team's aggregate
|
|
19
|
+
failure remains a Phronomy::Error and now carries the stored code. An operation
|
|
20
|
+
that already succeeded with the same identity still replays its original result.
|
|
21
|
+
Unknown persistence outcomes and readback failures remain recovery requirements;
|
|
22
|
+
an inner code is not blindly copied onto that outer uncertainty.
|
|
23
|
+
|
|
24
|
+
The application may choose to retry only a durably failed planning attempt:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
outcome && outcome[:status] == "failed" &&
|
|
28
|
+
outcome.dig(:error, "code") == "team.enqueue_after_finalize"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Example 21 retains its four-section/content-length validator and existing retry
|
|
32
|
+
limit. That application policy is not part of the reason code contract.
|
|
33
|
+
|
|
34
|
+
## Compatibility
|
|
35
|
+
|
|
36
|
+
No SQL columns or record-format versions change. Error diagnostics are content
|
|
37
|
+
JSON; the existing content codec carries the optional field without interpreting
|
|
38
|
+
it. Old diagnostics without codes remain readable. Unknown string codes are
|
|
39
|
+
preserved, and no code is inferred from a message.
|
|
40
|
+
|
|
41
|
+
A v0.28.0 reader can inspect the extra JSON field through stored result data.
|
|
42
|
+
Its reconstructed exceptions do not expose the new code, and paths that rebuild
|
|
43
|
+
a failure from class/message can lose it. Mixed-version writes and code-based
|
|
44
|
+
exception handling on an old reader are therefore unsupported. Upgrade consumers
|
|
45
|
+
before relying on codes; there is no automatic historical backfill.
|
data/docs/features.md
CHANGED
|
@@ -30,6 +30,14 @@ for production deployments.
|
|
|
30
30
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
31
31
|
| **Tracing** — Pluggable logical-operation span observability with bounded automatic coverage | Stable |
|
|
32
32
|
| **Error Taxonomy** — Provider errors translated to Phronomy transport/authentication/rate-limit/context errors | Beta |
|
|
33
|
+
| **Optional failure reason codes** — `Phronomy::Error#code` and `code:` retain domain-supplied reasons in saved Agent/Team diagnostics; [meaning and compatibility](failure-reason-codes.md) | Beta |
|
|
34
|
+
|
|
35
|
+
The RubyLLM backends also translate identified Faraday timeout and connection
|
|
36
|
+
failures to the owning `LLMAdapter::TransportError` or
|
|
37
|
+
`Embeddings::TransportError`, retaining the original cause chain. A stream may
|
|
38
|
+
have delivered partial output before failure; this classification does not imply
|
|
39
|
+
that retry is safe. Callback failures and execution cancellation/deadlines keep
|
|
40
|
+
their identity. See [transport error boundaries](architecture/transport-error-boundaries.md).
|
|
33
41
|
|
|
34
42
|
Agent definition lineage is separate from Agent instance identity. A named
|
|
35
43
|
concrete Agent may declare `agent_definition version: N`; when `id:` is omitted,
|
|
@@ -51,6 +59,7 @@ rather than implicitly inheriting the parent revision. The Stable
|
|
|
51
59
|
| **VectorStore::Embeddings::AsyncClient** — `embed_async` offloads synchronous `embed` through Phronomy and returns `TaskResult` | Beta |
|
|
52
60
|
| **Model Context Protocol (MCP) Tool** — `Phronomy::Tools::Mcp` integration through the official `mcp` gem | Beta |
|
|
53
61
|
| **Agent Tool** — `Phronomy::Tools::Agent.from_agent` exposes a child Agent as a Tool without occupying a worker while waiting | Beta |
|
|
62
|
+
| **Ordered event delivery** — Explicitly require `Phronomy::Integrations::OrderedEventDelivery` for bounded ordered delivery outside lifecycle callbacks; [lifetime and failure rules](ordered-event-delivery.md) | Beta |
|
|
54
63
|
| **Vector Search Tool** — `Phronomy::Tools::VectorSearch` wraps VectorStore and Embeddings adapters | Beta |
|
|
55
64
|
|
|
56
65
|
Applications previously calling async methods on backend objects must
|
|
@@ -63,6 +72,7 @@ Backend authors continue to implement the synchronous SPI only.
|
|
|
63
72
|
|---|---|
|
|
64
73
|
| **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions; single writer for Phronomy-managed live Agent execution state | Beta |
|
|
65
74
|
| **Agent execution result authority** — Session-local routing plus current FSM state and purpose-specific semantic IDs (`llm_call_id`, `tool_invocation_id`) reject stale async results | Beta |
|
|
75
|
+
| **Agent scope and unload** — `Agent.open_scope` owns newly acquired Agents; `unload_async` / `unload!` release idle incarnations without changing saved state, with typed invalidation and cleanup failures; [contract](architecture/dx05-agent-scope.md) | Beta |
|
|
66
76
|
| **Agent live ownership/admission** — Runtime owns one mutable live Agent per `agent_id`; EventLoop rejects competing same-Agent top-level executions while suspension retains the logical execution slot; Persistence admission remains durable defense | Beta |
|
|
67
77
|
| **Workflow durable admission** — Same-process admission is keyed by durable `workflow_instance_id` from pre-hydration admission through the durable terminal/halted save barrier; admission-owner representation is Runtime-internal | Beta |
|
|
68
78
|
| **`invoke` / `invoke_async`** — Blocking and non-blocking Agent/Workflow entry points | Stable |
|
|
@@ -92,9 +102,9 @@ See the [migration guide](migrations/parallel-tool-chat-removal.md).
|
|
|
92
102
|
|
|
93
103
|
| Feature | Stability |
|
|
94
104
|
|---|---|
|
|
95
|
-
| **Workflow asynchronous pattern** —
|
|
105
|
+
| **Workflow asynchronous pattern** — `task_state` starts a `TaskResult`, applies its successful value once to the current state entry, and then evaluates ordinary transitions; `Workflow#signal` remains the application event API | Beta |
|
|
96
106
|
| **Durable Handoff** — `MultiAgent::Handoff` and `MultiAgent::HandoffRunner` persist responsibility, immutable Context and exact Target reservation in one Persistence domain; current graph wiring is required for continuation | Beta |
|
|
97
|
-
| **GeneratorVerifier** — Generator-Verifier loop with
|
|
107
|
+
| **GeneratorVerifier** — Generator-Verifier loop with application prompts, Agent-validated output schemas and explicit output failures | Beta |
|
|
98
108
|
| **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
99
109
|
| **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
|
|
100
110
|
| **`Phronomy::MultiAgent::SharedState`** — Sequential peer-agent coordination through an invocation-local findings store; see the [namespace migration](migrations/shared-state-multi-agent.md) | Experimental |
|
|
@@ -190,3 +200,5 @@ claim. See [the migration](migrations/durable-semantic-coordination-v2.md).
|
|
|
190
200
|
turn read-only, including a current absent-but-reserved Target, without graph or
|
|
191
201
|
Agent hydration. Purging an idle main Agent removes its Handoff anchor; pending
|
|
192
202
|
coordination rejects purge rather than discarding its reservation.
|
|
203
|
+
|
|
204
|
+
Structured output and symmetric lifetime details: [contract and migration](architecture/structured-output-and-scope.md).
|
data/docs/getting-started.md
CHANGED
|
@@ -4,6 +4,11 @@ This guide contains the setup and introductory examples that were previously
|
|
|
4
4
|
embedded in the repository README. The README remains the project entry point;
|
|
5
5
|
this document carries the longer operational examples.
|
|
6
6
|
|
|
7
|
+
Blocks marked `ruby runnable` are complete after `require "phronomy"` and provider
|
|
8
|
+
configuration. Other blocks are setup or partial examples that reuse definitions
|
|
9
|
+
from the surrounding section. See [application recipes](application-recipes.md)
|
|
10
|
+
for independent examples and explicit failure handling.
|
|
11
|
+
|
|
7
12
|
## Install
|
|
8
13
|
|
|
9
14
|
Add Phronomy to your Gemfile:
|
|
@@ -49,11 +54,9 @@ Install only the backend gems required by your application:
|
|
|
49
54
|
|
|
50
55
|
## Define a Tool and Agent
|
|
51
56
|
|
|
52
|
-
Use `Phronomy::Tool::Base` as the application-facing authoring API.
|
|
53
|
-
exact alias of the existing `Phronomy::Tool::Base`, so
|
|
54
|
-
existing Tool definitions using the longer namespace remain compatible.
|
|
57
|
+
Use `Phronomy::Tool::Base` as the application-facing authoring API.
|
|
55
58
|
|
|
56
|
-
```ruby
|
|
59
|
+
```ruby runnable
|
|
57
60
|
class WebSearch < Phronomy::Tool::Base
|
|
58
61
|
description "Search the web"
|
|
59
62
|
param :query, type: :string, desc: "Search query"
|
|
@@ -115,7 +118,7 @@ A Runtime has at most one mutable live Agent object for an `agent_id`. If the
|
|
|
115
118
|
Agent is already live, repeated `load` returns that exact Ruby object and does
|
|
116
119
|
not reload Persistence. If it is not live, `load` hydrates the durable Agent
|
|
117
120
|
once. Loading an ID that does not exist durably raises
|
|
118
|
-
`
|
|
121
|
+
`Phronomy::Persistence::NotFoundError`.
|
|
119
122
|
|
|
120
123
|
Use `get` when only a process-local lookup is wanted:
|
|
121
124
|
|
|
@@ -131,7 +134,7 @@ While the Agent is live, it owns its current AgentRoot/Journal view and
|
|
|
131
134
|
Runtime/EventLoop owns process-local identity/admission/execution authority.
|
|
132
135
|
Phronomy persists snapshots at defined durability boundaries but does not reload
|
|
133
136
|
mutable Agent/Execution/Journal state before every LLM or Tool step. A conflicting
|
|
134
|
-
external durable write is surfaced as `
|
|
137
|
+
external durable write is surfaced as `Phronomy::Persistence::StateConflictError` rather than
|
|
135
138
|
silently merged into the live instance.
|
|
136
139
|
|
|
137
140
|
The active transcript and Knowledge views can be advanced independently without
|
|
@@ -165,7 +168,7 @@ the listener when the Agent is materialized, not on each invocation:
|
|
|
165
168
|
|
|
166
169
|
```ruby
|
|
167
170
|
agent = ResearchAgent.load(
|
|
168
|
-
"
|
|
171
|
+
"another-session-not-live-in-this-runtime",
|
|
169
172
|
persistence: persistence,
|
|
170
173
|
on_event: ->(event) {
|
|
171
174
|
puts event.payload[:output] if event.type == :done
|
|
@@ -175,6 +178,10 @@ agent = ResearchAgent.load(
|
|
|
175
178
|
task = agent.invoke_async("Hello")
|
|
176
179
|
```
|
|
177
180
|
|
|
181
|
+
This load assumes that the other Agent exists in Persistence but has not been
|
|
182
|
+
materialized in this Runtime. To listen to `research-session-42`, register its
|
|
183
|
+
listener in the original `create` call.
|
|
184
|
+
|
|
178
185
|
`new`, `create`, and `load` also accept an equivalent listener block.
|
|
179
186
|
Supplying both `on_event:` and a construction block is an error. If `load`
|
|
180
187
|
resolves an already-live same-process Agent, supplying any new listener/block
|
|
@@ -299,18 +306,16 @@ not inject an explicit `persistence:`. Workflow durability is fixed at the
|
|
|
299
306
|
application/Workflow-definition boundary; there is no per-invocation backend
|
|
300
307
|
switch.
|
|
301
308
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
Use `task_state` when a state starts an asynchronous operation and needs its
|
|
310
|
+
terminal result before proceeding. `start` returns the TaskResult immediately;
|
|
311
|
+
`apply` updates the current Workflow context on EventLoop. Guards run after
|
|
312
|
+
successful application. Ordinary entry and transition actions remain synchronous.
|
|
306
313
|
|
|
307
|
-
```ruby
|
|
314
|
+
```ruby runnable
|
|
308
315
|
class AnswerContext
|
|
309
316
|
include Phronomy::WorkflowContext
|
|
310
|
-
|
|
311
317
|
field :question, type: :replace, default: ""
|
|
312
|
-
field :answer,
|
|
313
|
-
# workflow_instance_id is framework-owned metadata; do not declare it as a field.
|
|
318
|
+
field :answer, type: :replace, default: nil
|
|
314
319
|
end
|
|
315
320
|
|
|
316
321
|
class ResearchAgent < Phronomy::Agent::Base
|
|
@@ -320,40 +325,35 @@ class ResearchAgent < Phronomy::Agent::Base
|
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
my_agent = ResearchAgent.new
|
|
323
|
-
workflow = nil
|
|
324
|
-
|
|
325
328
|
workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
326
329
|
initial :asking
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
task_state :asking,
|
|
331
|
+
start: ->(state) { my_agent.invoke_async(state.question) },
|
|
332
|
+
apply: ->(state, result) { state.merge(answer: result.fetch(:output)) }
|
|
333
|
+
transition from: :asking, to: :__finish__
|
|
334
|
+
end
|
|
332
335
|
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
answer = workflow.invoke(
|
|
337
|
+
{question: "What is an asynchronous completion handle?"},
|
|
338
|
+
config: {workflow_instance_id: "answer-example"}
|
|
339
|
+
)
|
|
340
|
+
puts answer.answer
|
|
341
|
+
```
|
|
335
342
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
end
|
|
343
|
+
Phronomy correlates completion to this execution and state entry, ignores stale
|
|
344
|
+
results after exit or reentry, and propagates start/source/apply failures to the
|
|
345
|
+
caller. A cancelled source cancels the Workflow observation; a failed source
|
|
346
|
+
retains its failure classification. External business events still use
|
|
347
|
+
`Workflow#signal` or `send_event`.
|
|
342
348
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
to: :done,
|
|
350
|
-
action: ->(ctx, event) { ctx.merge(answer: event.payload[:answer]) }
|
|
351
|
-
)
|
|
352
|
-
end
|
|
353
|
-
```
|
|
349
|
+
`task_state` is a live wait, while `wait_state` halts the current execution segment.
|
|
350
|
+
Pending Tasks are not persisted: attempting to restore a task-state snapshot
|
|
351
|
+
raises `WorkflowTaskResumeError`. Normal wait-state halt/resume remains supported.
|
|
352
|
+
Parent cancellation stops application of the result; cancelling the child itself
|
|
353
|
+
requires explicit controls passed by the application. See the
|
|
354
|
+
[task-state contract](architecture/dx05-workflow-task-state.md) for these boundaries.
|
|
354
355
|
|
|
355
|
-
Returning a
|
|
356
|
-
implicit await mechanism and is rejected.
|
|
356
|
+
Returning a TaskResult from an ordinary entry/transition action remains an error.
|
|
357
357
|
|
|
358
358
|
## Agent as Tool
|
|
359
359
|
|
|
@@ -435,6 +435,7 @@ contracts.
|
|
|
435
435
|
## Next steps
|
|
436
436
|
|
|
437
437
|
- [Features and API stability](features.md)
|
|
438
|
+
- [Application recipes](application-recipes.md)
|
|
438
439
|
- [Runtime and concurrency](runtime-and-concurrency.md)
|
|
439
440
|
- [Architecture decisions](decisions/)
|
|
440
441
|
- [Migration guides](migrations/)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# r8 unit15 migration
|
|
2
2
|
|
|
3
|
+
> Historical contract: parser injection/fallback is superseded by
|
|
4
|
+
> [structured output and scope](../architecture/structured-output-and-scope.md).
|
|
5
|
+
|
|
3
6
|
## Application impact
|
|
4
7
|
|
|
5
8
|
No public API, data migration or configuration changes are required.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Ordered event delivery
|
|
2
|
+
|
|
3
|
+
Load the application integration explicitly:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require "phronomy"
|
|
7
|
+
require "phronomy/integrations/ordered_event_delivery"
|
|
8
|
+
|
|
9
|
+
Phronomy::Integrations::OrderedEventDelivery.open(
|
|
10
|
+
capacity: 256, batch_size: 32, flush_timeout: 30,
|
|
11
|
+
deliver: ->(payload) { send_to_application_transport(payload) }
|
|
12
|
+
) do |delivery|
|
|
13
|
+
agent = MyAgent.new(on_event: ->(event) {
|
|
14
|
+
payload = application_payload(event)
|
|
15
|
+
delivery.publish(payload) if payload
|
|
16
|
+
})
|
|
17
|
+
agent.stream("Hello")
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The block runs on the external caller, and its result is returned after drain.
|
|
22
|
+
`open` rejects EventLoop callers before executing their block. Publication only
|
|
23
|
+
snapshots and queues plain data; it does not wait for capacity or send network
|
|
24
|
+
traffic. The existing Blocking API supplies a worker only while draining. The
|
|
25
|
+
integration creates no scheduler and never shuts down the shared Runtime/pool.
|
|
26
|
+
|
|
27
|
+
The app owns authentication, destination, authorization, Rails executor wrapping,
|
|
28
|
+
event-to-payload conversion, and any token coalescing. `prepare_batch:` optionally
|
|
29
|
+
receives an immutable Array of snapshots on the drain worker and returns an Array
|
|
30
|
+
of payloads to send in order. Its default is no transformation. Example 18 passes
|
|
31
|
+
its own TokenEventBatch, preserving adjacent-token coalescing with equal metadata.
|
|
32
|
+
|
|
33
|
+
Capacity limits the waiting queue, excluding an already extracted batch. Multiple
|
|
34
|
+
publishers are ordered by queue admission, not by thread start time. Hashes,
|
|
35
|
+
Arrays, Strings and scalar plain values are copied into immutable snapshots;
|
|
36
|
+
cycles, non-finite numbers and arbitrary objects fail publication.
|
|
37
|
+
|
|
38
|
+
On overflow/invalid publication, acceptance closes and the failure remains visible
|
|
39
|
+
to final drain even if an Agent listener merely logs its exception. Previously
|
|
40
|
+
accepted work drains if transport is healthy. A failed batch conversion, delivery
|
|
41
|
+
or worker admission also closes acceptance; remaining work may not be delivered.
|
|
42
|
+
There is no silent drop-as-success, automatic retry or durable delivery guarantee.
|
|
43
|
+
|
|
44
|
+
`close_and_wait` can be called by multiple external threads. It waits for physical
|
|
45
|
+
drain completion, not just TaskResult settlement. A timeout ends only the wait;
|
|
46
|
+
a network operation can still complete afterwards. Configure transport timeouts
|
|
47
|
+
in the transport itself. Network failure does not prove that nothing was sent.
|
|
48
|
+
|
|
49
|
+
If application execution fails, its original exception, message and backtrace
|
|
50
|
+
remain primary after cleanup. A simultaneous delivery failure is reported through
|
|
51
|
+
the configured Phronomy logger. Without a logger there is no durable diagnostic
|
|
52
|
+
guarantee; logger failure falls back to stderr without replacing the primary error.
|
|
53
|
+
If only delivery fails, that failure is raised. The small final-answer-only Job
|
|
54
|
+
needs no queue and is unchanged.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# A bounded Workflow completion recipe
|
|
2
|
+
|
|
3
|
+
This is a **legacy manual-signal recipe**, retained for compatibility and its
|
|
4
|
+
bounded-lifetime tests. New code should use
|
|
5
|
+
[`task_state`](architecture/dx05-workflow-task-state.md), which owns completion
|
|
6
|
+
correlation and failure propagation for operations started by a Workflow state.
|
|
7
|
+
The helper below is application code and is not a Phronomy integration API.
|
|
8
|
+
Its limitations do not describe the new task-state API.
|
|
9
|
+
|
|
10
|
+
Use the application-local function below only for **one live waiting stage, with
|
|
11
|
+
no restart, reentry or replacement while its operation is pending**. It separates
|
|
12
|
+
result mapping (application logic) from the repetitive notification plumbing.
|
|
13
|
+
For repeated/resumed stages, the app must put an operation ID in every success
|
|
14
|
+
AND error payload and check it with a receiver-side transition `guard:` before
|
|
15
|
+
applying the payload. Signal admission alone is not that check. The example
|
|
16
|
+
below intentionally does not claim that wider guarantee.
|
|
17
|
+
|
|
18
|
+
```ruby runnable
|
|
19
|
+
def forward_completion(task, workflow:, workflow_instance_id:, event:, on_delivery_error:)
|
|
20
|
+
task.on_complete do |payload, operation_error|
|
|
21
|
+
delivery_error = nil
|
|
22
|
+
begin
|
|
23
|
+
accepted = workflow.signal(
|
|
24
|
+
workflow_instance_id: workflow_instance_id, event: event,
|
|
25
|
+
payload: operation_error ? {error: operation_error} : payload
|
|
26
|
+
)
|
|
27
|
+
delivery_error = Phronomy::Error.new("Workflow did not accept #{event}") unless accepted
|
|
28
|
+
rescue => error
|
|
29
|
+
delivery_error = error
|
|
30
|
+
end
|
|
31
|
+
on_delivery_error.call(delivery_error) if delivery_error
|
|
32
|
+
end
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class RecipeAnswerAgent < Phronomy::Agent::Base
|
|
37
|
+
agent_definition id: "bounded-completion-recipe", version: 1
|
|
38
|
+
model "gpt-4o-mini"
|
|
39
|
+
instructions "Answer briefly."
|
|
40
|
+
end
|
|
41
|
+
class CompletionState
|
|
42
|
+
include Phronomy::WorkflowContext
|
|
43
|
+
field :question, type: :replace
|
|
44
|
+
field :answer, type: :replace
|
|
45
|
+
field :error_message, type: :replace
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
agent = RecipeAnswerAgent.new
|
|
49
|
+
errors = Queue.new
|
|
50
|
+
workflow = nil
|
|
51
|
+
workflow = Phronomy::Workflow.define(CompletionState) do
|
|
52
|
+
initial :asking
|
|
53
|
+
state :asking
|
|
54
|
+
state :done
|
|
55
|
+
entry :asking, ->(state) {
|
|
56
|
+
task = agent.invoke_async(state.question).map { |result| {answer: result.fetch(:output)} }
|
|
57
|
+
forward_completion(task, workflow: workflow,
|
|
58
|
+
workflow_instance_id: state.workflow_instance_id, event: :answer_ready,
|
|
59
|
+
on_delivery_error: ->(error) { errors << error })
|
|
60
|
+
state
|
|
61
|
+
}
|
|
62
|
+
transition from: :asking, on: :answer_ready, to: :done,
|
|
63
|
+
action: ->(state, event) {
|
|
64
|
+
error = event.payload[:error]
|
|
65
|
+
error ? state.merge(error_message: error.message) : state.merge(answer: event.payload.fetch(:answer))
|
|
66
|
+
}
|
|
67
|
+
transition from: :done, to: :__finish__
|
|
68
|
+
end
|
|
69
|
+
final = workflow.invoke({question: "Explain completion handles"},
|
|
70
|
+
config: {workflow_instance_id: "bounded-recipe"})
|
|
71
|
+
puts final.error_message || final.answer
|
|
72
|
+
raise errors.pop unless errors.empty?
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`forward_completion` returns nil after registering one callback. Already-completed
|
|
76
|
+
results are notified immediately. Two registrations are two subscriptions; the
|
|
77
|
+
application avoids duplicates. No task state is changed, no retry is attempted,
|
|
78
|
+
and no callback waits for execution, storage or network delivery.
|
|
79
|
+
|
|
80
|
+
Operation failures and cancellation notify the Workflow with `{error: error}`.
|
|
81
|
+
A false signal result and a raised signal exception each notify the required
|
|
82
|
+
`on_delivery_error` callback once. A failure of that callback follows TaskResult's
|
|
83
|
+
existing logger policy; it is not retried and does not rewrite the task result.
|
|
84
|
+
The application observes delivery errors and chooses cancellation/recovery.
|
|
85
|
+
|
|
86
|
+
Signal acceptance does not guarantee transition completion or a durable save.
|
|
87
|
+
Incomplete tasks retain their subscriptions until settlement under the existing
|
|
88
|
+
TaskResult lifecycle. The caller still owns operation cancellation and lifetime;
|
|
89
|
+
this recipe installs no Runtime shutdown hook or global subscription registry.
|
|
90
|
+
|
|
91
|
+
See [the full application recipe](application-recipes.md#workflow-completion-and-failures)
|
|
92
|
+
for the same failure behavior without extracting this application-local function.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
eval_gemfile "mcp_1_0.gemfile"
|
|
4
|
+
gem "faraday", "= 2.14.4"
|
|
5
|
+
gem "faraday-net_http", "= 3.4.4"
|
|
6
|
+
gem "webmock", require: false
|
|
7
|
+
|
|
8
|
+
# Strict JSON decoding and schema transport use these verified minimum versions.
|
|
9
|
+
gem "json", "= 2.21.2"
|
|
10
|
+
gem "ruby_llm", "= 2.0.0"
|
|
@@ -47,7 +47,7 @@ module Phronomy
|
|
|
47
47
|
"reservation" => correlation, "current_input_ref" => input_ref,
|
|
48
48
|
"durable_context_ref" => context_ref,
|
|
49
49
|
"execution_extension" => @config[:phronomy_execution_participant]&.binding&.to_h
|
|
50
|
-
}.merge(@preparation_metadata).compact)
|
|
50
|
+
}.merge(@preparation_metadata).merge(@config[:phronomy_output_contract]&.metadata(records.contents) || {}).compact)
|
|
51
51
|
input_record = JournalRecord.from_h(input_record.to_h.merge("execution_id" => execution_id))
|
|
52
52
|
execution = execution.with(execution_revision: 0, working_records: [input_record])
|
|
53
53
|
records.executions.create_active(execution)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../persistence/errors"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
# Only the requested Agent root is absent, not one of its referenced records.
|
|
7
|
+
# @api public
|
|
8
|
+
class AgentNotFoundError < Phronomy::Persistence::NotFoundError
|
|
9
|
+
attr_reader :agent_id
|
|
10
|
+
|
|
11
|
+
def initialize(agent_id:)
|
|
12
|
+
@agent_id = agent_id.to_s.dup.freeze
|
|
13
|
+
super("Agent not found: #{@agent_id}", code: "agent.not_found")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# An old Agent reference cannot act on a later incarnation of the same ID.
|
|
5
|
+
# @api public
|
|
6
|
+
class AgentUnloadedError < Phronomy::Error
|
|
7
|
+
attr_reader :agent_id
|
|
8
|
+
|
|
9
|
+
def initialize(agent_id:)
|
|
10
|
+
@agent_id = agent_id.to_s.dup.freeze
|
|
11
|
+
super("Agent #{@agent_id.inspect} has been unloaded", code: "agent.unloaded")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|