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.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +32 -6
  4. data/docs/application-recipes.md +229 -0
  5. data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
  6. data/docs/architecture/agent-iteration-budget.md +81 -0
  7. data/docs/architecture/dx05-agent-scope.md +126 -0
  8. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  9. data/docs/architecture/error-api-review.md +245 -0
  10. data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
  11. data/docs/architecture/generator-verifier-ownership.md +18 -109
  12. data/docs/architecture/llm-continuation-metadata.md +88 -0
  13. data/docs/architecture/lm-studio-output-failures.md +111 -0
  14. data/docs/architecture/r8-unit15.md +3 -0
  15. data/docs/architecture/r8-unit7.md +2 -1
  16. data/docs/architecture/structured-output-and-scope.md +183 -0
  17. data/docs/architecture/tracing.md +22 -4
  18. data/docs/architecture/transport-error-boundaries.md +103 -0
  19. data/docs/architecture/trust-acceptance-followup.md +87 -0
  20. data/docs/failure-reason-codes.md +45 -0
  21. data/docs/features.md +14 -2
  22. data/docs/getting-started.md +44 -43
  23. data/docs/migrations/r8-unit15.md +3 -0
  24. data/docs/ordered-event-delivery.md +54 -0
  25. data/docs/workflow-completion.md +92 -0
  26. data/gemfiles/http_minimum.gemfile +10 -0
  27. data/lib/phronomy/agent/admission.rb +1 -1
  28. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  29. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  30. data/lib/phronomy/agent/api/agent.rb +13 -0
  31. data/lib/phronomy/agent/async_event_api.rb +121 -88
  32. data/lib/phronomy/agent/base.rb +82 -44
  33. data/lib/phronomy/agent/before_llm_input.rb +7 -1
  34. data/lib/phronomy/agent/context_importer.rb +4 -0
  35. data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
  36. data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
  37. data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
  38. data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
  39. data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
  40. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
  41. data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
  42. data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
  43. data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
  44. data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
  45. data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
  46. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
  47. data/lib/phronomy/agent/execution_observation.rb +5 -5
  48. data/lib/phronomy/agent/filterable.rb +21 -15
  49. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  50. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  51. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  52. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
  53. data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
  54. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
  55. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  56. data/lib/phronomy/agent/output_contract.rb +64 -0
  57. data/lib/phronomy/agent/output_error.rb +53 -0
  58. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
  59. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
  60. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
  61. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
  62. data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
  63. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
  64. data/lib/phronomy/agent/runtime_input.rb +15 -4
  65. data/lib/phronomy/agent/saved_context_reader.rb +2 -1
  66. data/lib/phronomy/agent/store.rb +10 -2
  67. data/lib/phronomy/agent/stream_callback_error.rb +2 -2
  68. data/lib/phronomy/common/error.rb +65 -1
  69. data/lib/phronomy/common/values/immutable.rb +33 -0
  70. data/lib/phronomy/content_store/base.rb +7 -2
  71. data/lib/phronomy/context/assembly.rb +18 -22
  72. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  73. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
  74. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  75. data/lib/phronomy/engine/fsm_session.rb +16 -1
  76. data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
  77. data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
  78. data/lib/phronomy/execution/task_result.rb +7 -2
  79. data/lib/phronomy/filter/filter_block_error.rb +2 -2
  80. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
  81. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
  82. data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
  83. data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
  84. data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
  85. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
  86. data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
  87. data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
  88. data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
  89. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
  90. data/lib/phronomy/generation/generator_verifier.rb +170 -85
  91. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  92. data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
  93. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
  94. data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
  95. data/lib/phronomy/llm_adapter/message.rb +4 -0
  96. data/lib/phronomy/llm_adapter/request.rb +17 -2
  97. data/lib/phronomy/llm_adapter/response.rb +11 -4
  98. data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
  99. data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
  100. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
  101. data/lib/phronomy/multi_agent/shared_state.rb +1 -0
  102. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  103. data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
  104. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  105. data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
  106. data/lib/phronomy/output_parser/parse_error.rb +10 -1
  107. data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
  108. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  109. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  110. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
  111. data/lib/phronomy/tool/base.rb +25 -23
  112. data/lib/phronomy/tracing/automatic.rb +14 -8
  113. data/lib/phronomy/tracing/base.rb +37 -7
  114. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
  115. data/lib/phronomy/tracing/observation.rb +2 -1
  116. data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
  117. data/lib/phronomy/version.rb +1 -1
  118. data/lib/phronomy/workflow/execution/workflow.rb +30 -2
  119. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
  120. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
  121. data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
  122. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  123. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  124. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  125. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
  126. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
  127. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  128. data/lib/phronomy.rb +7 -2
  129. data/scripts/api_snapshot.rb +15 -0
  130. data/scripts/check_readme_ruby.rb +24 -29
  131. data/scripts/check_readme_runnable.rb +92 -81
  132. data/scripts/migrate_generator_verifier_result.rb +18 -0
  133. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  134. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  135. data/sig/phronomy/agent/execution_contract.rbs +4 -3
  136. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  137. data/sig/phronomy/agent.rbs +36 -5
  138. data/sig/phronomy/embeddings/base.rbs +6 -0
  139. data/sig/phronomy/execution/execution.rbs +2 -2
  140. data/sig/phronomy/extensions.rbs +8 -2
  141. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  142. data/sig/phronomy/generation/generator_verifier.rbs +70 -0
  143. data/sig/phronomy/integrations.rbs +14 -0
  144. data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
  145. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
  146. data/sig/phronomy/llm_adapter/values.rbs +8 -2
  147. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  148. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  149. data/sig/phronomy/storage/contracts.rbs +2 -2
  150. data/sig/phronomy/tracing/observation.rbs +1 -1
  151. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
  152. data/sig/phronomy/workflow.rbs +4 -0
  153. data/sig/phronomy.rbs +6 -0
  154. metadata +103 -5
  155. data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
  156. data/lib/phronomy/generation/low_confidence_error.rb +0 -14
  157. 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** — Start async work, return immediately, and continue through `Workflow#signal` | Beta |
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 injectable prompts/parsers | Beta |
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).
@@ -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. It is an
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
- `Storage::NotFoundError`.
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 `Storage::ConflictError` rather than
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
- "research-session-42",
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
- Workflow entry and transition actions are synchronous Run-to-Completion
303
- callbacks. If a Workflow needs an Agent or another asynchronous lifecycle, start
304
- it asynchronously, return the Workflow context immediately, and deliver its
305
- completion later with `Workflow#signal`.
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, type: :replace, default: nil
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
- state :asking
328
- state :done
329
-
330
- entry :asking, ->(ctx) {
331
- workflow_instance_id = ctx.workflow_instance_id
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
- my_agent.invoke_async(ctx.question) do |event|
334
- next unless event.type == :done
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
- workflow.signal(
337
- workflow_instance_id: workflow_instance_id,
338
- event: :answer_ready,
339
- payload: {answer: event.payload[:output]}
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
- ctx
344
- }
345
-
346
- transition(
347
- from: :asking,
348
- on: :answer_ready,
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 `Phronomy::TaskResult` from a Workflow entry/transition action is not an
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