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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f097c81530a33bd631974866ff26a0d2ec8b4793286c6dfa0bb0facaac4fdd83
4
- data.tar.gz: 80b9980737c80446f660d9a058aa1b898bdcdec452e517764261c386c32a1e78
3
+ metadata.gz: 46b13a1255f5821e321de0e3817f9fc2335ae2eba81dc7142a01295ffffcdd05
4
+ data.tar.gz: c268bfd441361880ce4c6c6a84f03e7e0530b5e1ca8736e87bfba78756a69c89
5
5
  SHA512:
6
- metadata.gz: 2cacf04ecea168021b36835f12901723380a7a908d0173f23b564e9bb67d4f85cf46bf958949fd4257043393234ef5acc7e8d9cecdcf68beac61b0c7ce454084
7
- data.tar.gz: c7f7b1d9cbcd54d0d0d9bb7583e5587ecadfafa987788f44e8c8abc0f9fa75241092d63a543669b55a6837fc1c2c09c102cf514578b6c266b2c85ff17d2ba2cc
6
+ metadata.gz: 9f53504e27f5cefedb2370ed495387c17cf98ae850b847497090b17fe1971545a565bfe4533368f47646c5953352aebc4f29801afdb3a2691a54927f60ec1df4
7
+ data.tar.gz: 4f9dace274b6a09ad4096569cd8a459dc74df63263c8d4fdc1455d6628e70c93464490a971c638a24aea844d09eb85f9f4ceafa64e9c9e39f5345daa6b83fffb
data/CHANGELOG.md CHANGED
@@ -12,7 +12,102 @@ Release history for 0.14.0 and earlier is archived in
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
- ### r8 first implementation unit (breaking, unreleased)
15
+ ## [0.30.0] - 2026-10-24
16
+
17
+ ### Added
18
+
19
+ - Agent `output_schema:` and `OutputParser::JsonSchemaParser` provide strict,
20
+ immutable JSON results. Refused, incomplete and invalid output raises
21
+ `Agent::OutputError`; native schema delivery is preserved through Tool rounds,
22
+ approval and recovery without output repair or replaying confirmed Tool work.
23
+ - Explicit `LLMAdapter::LMStudio` selection identifies the known generated-format
24
+ rejection as `llm.provider_output_parse_failed`, retaining the actual HTTP
25
+ status and original SDK cause. Opt-in retry resends the same eligible,
26
+ non-streaming gpt-oss-20b request at most once, without replaying Agent,
27
+ Workflow or Tool work. Ordinary SDK retries are disabled in this backend's
28
+ private snapshot; the default backend and global SDK settings are unchanged.
29
+
30
+ ### Changed
31
+
32
+ - GeneratorVerifier owns immutable evaluation Results, explicit approval routing
33
+ and Agent lifetime. Use `accepted?`, `termination_reason` and
34
+ `raise_if_unaccepted`; `output_failed` distinguishes invalid output from a
35
+ completed evaluation that reaches `iteration_limit`. Acceptance reports the
36
+ configured conditions, not the truth of generated content.
37
+ - Agent scopes and GeneratorVerifier scopes await a directly returned TaskResult
38
+ before releasing their owned Agents. Cleanup preserves the primary failure,
39
+ successful value and handles needed to retry release.
40
+ - Tracing usage is a plain token-count Hash. Custom tracer/exporter callers must
41
+ project LLM-domain TokenUsage values before crossing the tracing boundary.
42
+ - Require Faraday 2.14.4 and faraday-net_http 3.4.4 as the tested HTTP support
43
+ floors. Concrete LLM/Embeddings adapters translate recognized network failures
44
+ into their public TransportError types while preserving causes and callback
45
+ exceptions; cancellation and unknown internal failures are not broadly mapped.
46
+ Strict JSON decoding requires JSON >= 2.21.2 and < 3.
47
+
48
+ ### Removed
49
+
50
+ - GeneratorVerifier parser fallback/repair hooks, `trusted`/`trusted?`,
51
+ `raise_if_untrusted` and `LowConfidenceError`. Migrate saved Result data with
52
+ `scripts/migrate_generator_verifier_result.rb`; no compatibility aliases are
53
+ provided. See the [Result and lifetime contract](docs/architecture/generator-verifier-lifecycle-result.md).
54
+
55
+ ### Fixed
56
+
57
+ - Preserve each SharedState member's explicitly configured model when creating
58
+ its internal instrumented Agent. Keep ordinary Agent inheritance and the
59
+ global fallback for members without an explicit model unchanged.
60
+ - Preserve canonical assistant-message metadata through Agent Tool rounds,
61
+ imports, persistence and recovery. The RubyLLM backend retains supported
62
+ thinking text/signatures and raw reasoning, including final streaming
63
+ responses, and delegates wire rendering to the existing SDK. Incompatible
64
+ continuation scope/version is rejected before inference; legacy messages
65
+ without this metadata remain usable. No database migration is required. See
66
+ [continuation compatibility](docs/architecture/llm-continuation-metadata.md).
67
+ - Enforce `Agent.max_iterations = N` as at most N logical LLM calls per
68
+ execution, including the first and final response. Remove the premature FSM
69
+ cutoff that stopped the ninth single-Tool round before call ten. Tool count,
70
+ approval/resume, streaming chunks and SDK HTTP retries do not consume extra
71
+ logical calls; this limit does not bound physical HTTP attempts or Tool count.
72
+ Preserve the admitted limit and consumed-call records across approval,
73
+ recovery and a fresh Runtime/database pool. Legacy executions without the
74
+ saved limit use the current class setting. A final response on call N succeeds;
75
+ Tools requested on call N retain their results, and call N+1 raises the existing
76
+ `RecursionLimitError`. Zero permits no call; invalid limits fail before inference.
77
+ Generic Engine transition protection remains in place. This correction can
78
+ allow work previously stopped too early and stop runs previously able to reset
79
+ their budget on approval. Deploy at a quiescent Runtime boundary; see
80
+ [iteration budget migration](docs/architecture/agent-iteration-budget.md).
81
+
82
+ ### Verification and migration
83
+
84
+ - Real LM Studio acceptance identified HTTP 400 PEG failures, performed one
85
+ identical-request retry, and returned the final error at the adapter's own
86
+ limit after the second failure. A separate application invocation succeeded
87
+ initially with HTTP 200: two application invocations, three generation POSTs.
88
+ This is not recovery by retry within one invocation. Recovery by retry,
89
+ recovery rate, the PEG root cause and recurrence resolution remain unverified.
90
+ - Review the [structured-output migration](docs/architecture/structured-output-and-scope.md),
91
+ [continuation compatibility](docs/architecture/llm-continuation-metadata.md),
92
+ [iteration budget migration](docs/architecture/agent-iteration-budget.md) and
93
+ [LM Studio retry policy](docs/architecture/lm-studio-output-failures.md) before
94
+ upgrading persistent or in-flight applications.
95
+
96
+ ---
97
+
98
+ ## [0.29.0] - 2026-10-13
99
+
100
+ See the [0.29.0 release record](https://github.com/Raizo-TCS/phronomy/releases/tag/v0.29.0).
101
+
102
+ ---
103
+
104
+ ## [0.28.0] - 2026-10-09
105
+
106
+ The architecture notes below were already included in this release and carried
107
+ unchanged in the 0.29.0 changelog. They are retained from the former Unreleased
108
+ section and are not new 0.30.0 changes.
109
+
110
+ ### r8 first implementation unit (breaking)
16
111
 
17
112
  - Move Context policy/plan/manifest/budget/prompt operations into `Phronomy::Context`.
18
113
  Agent retains snapshot, journal and lifecycle ownership. Manifest and execution
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  > This project is primarily developed and maintained by **AI coding agents**.
5
5
  > As a result, `main` receives frequent, large, and unannounced changes.
6
6
  > External contributors should expect significant churn and potential conflicts at any time.
7
+ > Correct responsibility boundaries and behavior take priority over backward compatibility.
8
+ > Obsolete APIs and compatibility-only paths are removed; update applications with the documented API changes.
7
9
  > We apologise for the instability this may cause.
8
10
 
9
11
  **Phronomy** is a Ruby AI agent framework for stateful Agents, Workflows, Tools,
@@ -21,7 +23,7 @@ tracking `main` directly.
21
23
 
22
24
  - **Agent** — stateful, persistence-backed LLM agent with canonical execution history.
23
25
  - **Persistence** — shared atomic scopes with domain-owned Agent/Team stores and Workflow checkpoints.
24
- - **Workflow** — state-machine-driven application workflow with explicit events and wait states.
26
+ - **Workflow** — state-machine-driven application workflow with task states, explicit events and wait states.
25
27
  - **Tool / Capability** — callable application capability exposed to an Agent; application-defined Tools subclass `Phronomy::Tool::Base`.
26
28
  - **Multi-Agent Handoff** — semantic Source-to-Target responsibility transfer with policy-bounded Context projection and persisted active responsibility and exact Target recovery within one Persistence domain.
27
29
  - **EventLoop + FSMSession** — the framework control plane for logical lifecycle coordination.
@@ -35,6 +37,22 @@ tracking `main` directly.
35
37
 
36
38
  See [Features and Application Programming Interface (API) stability](docs/features.md) for the full feature matrix.
37
39
 
40
+ The following application APIs are available as Beta in v0.30.0:
41
+
42
+ | API | Stability | Contract |
43
+ |---|---|---|
44
+ | `Phronomy::LLMAdapter::LMStudio` | Beta | Explicit known-output-error identification and bounded opt-in retry; [policy and evidence limits](docs/architecture/lm-studio-output-failures.md) |
45
+ | Agent `output_schema:`, `Agent::OutputError` and `OutputParser::JsonSchemaParser` | Beta | Agent returns validated immutable JSON or an explicit output failure; [contract and migration](docs/architecture/structured-output-and-scope.md) |
46
+ | `Phronomy::GeneratorVerifier`, its approval/lifetime operations and immutable `Result` with output-failure diagnostics | Beta | [Generation and structured-output contract](docs/architecture/structured-output-and-scope.md) |
47
+ | `Agent.open_scope`, `Agent::Scope`, `Agent::Base#unload_async / #unload!`, `AgentUnloadedError` and `Agent::ScopeCleanupError` | Beta | Runtime-bound ownership and safe release of idle Agent incarnations without changing saved history or closing the Store; [contract](docs/architecture/dx05-agent-scope.md) |
48
+ | `Workflow::Builder#task_state` and `WorkflowTaskResumeError` | Beta | State-entry-scoped Task completion, live cancellation and explicit restart rejection; [contract](docs/architecture/dx05-workflow-task-state.md) |
49
+ | `Phronomy::Integrations::OrderedEventDelivery` | Beta | Optional ordered delivery with bounded admission and explicit drain lifetime; [usage](docs/ordered-event-delivery.md) |
50
+ | `Phronomy::Error#code` and error constructor `code:` | Beta | Optional reason codes and bounded causes retained in saved failure diagnostics; [meaning and compatibility](docs/architecture/error-api-review.md) |
51
+ | `Phronomy::AgentNotFoundError#agent_id` | Beta | Identifies absence of the requested Agent root; referenced history and Content failures retain their own errors; [migration](docs/architecture/error-api-review.md) |
52
+ | `Phronomy::MultiAgent::TeamNotFoundError` and `TeamBusyError`, with `team_id` | Beta | Team-owned absence and busy errors; child Agent failures retain their Agent classification; [migration](docs/architecture/error-api-review.md) |
53
+ | `Phronomy::Embeddings::AuthenticationError` and `RateLimitError` | Beta | Embeddings-owned transport error subclasses retaining the provider cause; [migration](docs/architecture/error-api-review.md) |
54
+ | `Phronomy::Tracing::Base.trace_with` | Beta | Uses the tracer start/finish SPI, attempts finish once, and preserves a primary application error; [contract](docs/architecture/error-api-review.md) |
55
+
38
56
  ## Installation
39
57
 
40
58
  Add Phronomy to your Gemfile:
@@ -49,12 +67,21 @@ Then run:
49
67
  bundle install
50
68
  ```
51
69
 
52
- This refactoring branch requires RubyLLM 2.0.x. See the
70
+ Phronomy requires RubyLLM 2.0.x. The tested minimum HTTP stack is Faraday
71
+ 2.14.4 and faraday-net_http 3.4.4; newer versions within their respective major
72
+ versions are allowed. Both floors are explicit: raising Faraday alone can still
73
+ resolve an old Net::HTTP adapter that loses HTTP status during streaming.
74
+ CI checks fresh resolution and this exact pair with MCP 1.0.0. These are verified
75
+ support floors, not a claim about the first upstream release containing a fix.
76
+ See the
53
77
  [RubyLLM 2 and token-ownership migration](docs/architecture/rubyllm-2-token-ownership.md)
54
78
  for removed input-budget overrides and the in-flight Tool-manifest upgrade boundary.
55
79
 
56
80
  Configure RubyLLM with the provider credentials and transport policy required by
57
- your application. Phronomy does not add another LLM transport retry/timeout layer.
81
+ your application. The default adapter delegates transport retry and timeout policy
82
+ to RubyLLM. The explicitly selected LMStudio backend instead disables SDK retries
83
+ and can retry one narrowly recognized generated-format failure at most once;
84
+ see its [policy](docs/architecture/lm-studio-output-failures.md).
58
85
 
59
86
  ```ruby
60
87
  RubyLLM.configure do |c|
@@ -91,9 +118,7 @@ result = ResearchAgent.new.invoke("What happened in AI research this week?")
91
118
  puts result[:output]
92
119
  ```
93
120
 
94
- `Phronomy::Tool::Base` is the public authoring name for the existing Tool base
95
- class. The legacy `Phronomy::Tool::Base` constant remains
96
- valid for compatibility.
121
+ `Phronomy::Tool::Base` is the public base class for application-defined Tools.
97
122
 
98
123
  For non-blocking top-level use, call `invoke_async` and keep the returned
99
124
  `Phronomy::TaskResult`. Inside Phronomy lifecycle callbacks, do not block waiting for
@@ -152,6 +177,7 @@ contracts, timeout/cancellation semantics, metrics, and callback rules.
152
177
  ## Documentation
153
178
 
154
179
  - [Getting started](docs/getting-started.md) — installation, RubyLLM setup, Agent/Workflow basics, persistence, streaming.
180
+ - [Application recipes](docs/application-recipes.md) — complete persistence, streaming, and Workflow examples, including explicit failure handling.
155
181
  - [Features and API stability](docs/features.md) — public feature matrix and stability labels.
156
182
  - [Architecture](docs/architecture.md) — canonical current explanatory architecture entry and authority navigation.
157
183
  - [Runtime and concurrency](docs/runtime-and-concurrency.md) — EventLoop, FSMSession, TaskResult, OffloadPool, cancellation, observability.
@@ -0,0 +1,229 @@
1
+ # Application recipes
2
+
3
+ Start with [Getting started](getting-started.md). Every `ruby runnable` block
4
+ below is a complete example after `require "phronomy"` and provider configuration.
5
+ CI substitutes only the LLM adapter; Agent, Tool and Workflow operations are real.
6
+
7
+ ## Choose the completion channel
8
+
9
+ | Need | Public API | Caller responsibility |
10
+ |---|---|---|
11
+ | One final answer | `agent.invoke(input)` | Call from an external thread |
12
+ | Nonblocking final answer | `agent.invoke_async(input)` | Observe the returned TaskResult |
13
+ | Intermediate events | Construction-time `on_event:` | Keep the callback short |
14
+ | Streamed tokens | `stream` / `stream_async` with a listener | Move network delivery out of the callback |
15
+ | Durable approval | `approve_async` with the request IDs | Apply the application's authorization decision |
16
+ | State-owned asynchronous result | `task_state(name, start:, apply:)` | Framework correlates completion to state entry |
17
+ | Live Workflow notification | `workflow.signal(...)` | Check admission; correlate the waiting operation |
18
+
19
+ `TaskResult#wait_result` waits at an external application boundary. Completion
20
+ callbacks and Agent listeners must not block EventLoop. A wait timeout does not
21
+ cancel the underlying operation. The caller owns cancellation and cleanup.
22
+
23
+ ## Create, load and get
24
+
25
+ ```ruby runnable
26
+ class ConversationAgent < Phronomy::Agent::Base
27
+ agent_definition id: "recipe-conversation", version: 1
28
+ model "gpt-4o-mini"
29
+ instructions "Answer briefly."
30
+ end
31
+
32
+ store = Phronomy::PersistenceComposition.in_memory.agent
33
+ agent = ConversationAgent.create(agent_id: "recipe-session", persistence: store)
34
+ puts agent.invoke("Hello").fetch(:output)
35
+
36
+ # A live lookup is not a database read. Loading a live identity returns its owner.
37
+ raise "Unexpected owner" unless ConversationAgent.get("recipe-session").equal?(agent)
38
+ raise "Unexpected load" unless ConversationAgent.load("recipe-session", persistence: store).equal?(agent)
39
+
40
+ def load_existing_agent(agent_class:, agent_id:, persistence:)
41
+ agent_class.load(agent_id, persistence: persistence)
42
+ rescue Phronomy::AgentNotFoundError => error
43
+ raise unless error.agent_id == agent_id.to_s
44
+ nil
45
+ end
46
+
47
+ missing = load_existing_agent(
48
+ agent_class: ConversationAgent, agent_id: "missing-session", persistence: store
49
+ )
50
+ raise "Expected a missing Agent" unless missing.nil?
51
+ ```
52
+
53
+ This application treats only absence as `nil`. Storage unavailability, state
54
+ conflicts and recovery requirements remain errors. `create` is not upsert; do
55
+ not catch every persistence failure and create a replacement conversation.
56
+ The in-memory store is for this example; a durable backend must outlive Runtime
57
+ to support process restart. See [persistence failures](migrations/persistence-failure-contracts.md).
58
+
59
+ ## Streaming and final results
60
+
61
+ ```ruby runnable
62
+ class StreamingAgent < Phronomy::Agent::Base
63
+ agent_definition id: "recipe-streaming", version: 1
64
+ model "gpt-4o-mini"
65
+ instructions "Explain briefly."
66
+ end
67
+
68
+ events = Queue.new
69
+ agent = StreamingAgent.new(on_event: ->(event) {
70
+ events << event if event.type == :token
71
+ })
72
+
73
+ task = agent.stream_async("Explain asynchronous results")
74
+ result = task.wait_result
75
+ puts result.fetch(:output)
76
+ puts "Received #{events.size} token events"
77
+ ```
78
+
79
+ The unbounded Queue is an in-process teaching example. A Web application needs
80
+ bounded admission and ordered delivery outside the listener, such as Example 18.
81
+ Register the listener at `new`/`create`/first `load`; per-invocation listener
82
+ arguments or blocks are rejected. Do not replace a listener by loading an
83
+ already-live Agent. See [approval](getting-started.md#human-in-the-loop-approval)
84
+ for the separate durable approval identifiers and application decision.
85
+
86
+ ## Workflow task states
87
+
88
+ When a Workflow owns the start of an operation, use `task_state` to keep the
89
+ application focused on its operation, result fields and routing decisions.
90
+ The example below returns the original error to its caller if either invocation
91
+ fails; it does not silently retry or convert a failed invocation into an answer.
92
+
93
+ ```ruby runnable
94
+ class TwoAnswersState
95
+ include Phronomy::WorkflowContext
96
+ field :question, type: :replace
97
+ field :answers, type: :replace, default: -> { [] }
98
+ end
99
+ class TwoAnswersAgent < Phronomy::Agent::Base
100
+ agent_definition id: "recipe-two-answers", version: 1
101
+ model "gpt-4o-mini"
102
+ instructions "Answer briefly."
103
+ end
104
+ agent = TwoAnswersAgent.new
105
+ workflow = Phronomy::Workflow.define(TwoAnswersState) do
106
+ initial :asking
107
+ task_state :asking,
108
+ start: ->(state) { agent.invoke_async(state.question) },
109
+ apply: ->(state, result) { state.merge(answers: state.answers + [result.fetch(:output)]) }
110
+ state :decide
111
+ transition from: :asking, to: :decide
112
+ transition from: :decide, to: :__finish__, guard: ->(state) { state.answers.length >= 2 }
113
+ transition from: :decide, to: :asking
114
+ end
115
+ puts workflow.invoke({question: "Explain completion handles"}).answers
116
+ ```
117
+
118
+ Expected business failures can be converted into application values before
119
+ returning the source Task, for example using `TaskResult.all_settled` and `map`.
120
+ Phronomy does not select a fallback or retry policy. See the
121
+ [full contract](architecture/dx05-workflow-task-state.md).
122
+
123
+ ## Workflow completion and failures
124
+
125
+ The following **legacy low-level signal recipe** is retained for existing users
126
+ and its compatibility tests. New state-owned asynchronous operations should use
127
+ `task_state` above. The restrictions below apply to this manual recipe, not to
128
+ the new task-state API.
129
+
130
+ This recipe targets **one live waiting stage**. Do not restart, reenter or replace
131
+ that stage while a result is pending. A logical `workflow_instance_id` alone does
132
+ not identify a particular execution or waiting operation. For those wider cases,
133
+ application operation IDs and receiver-side guards are necessary.
134
+
135
+ ```ruby runnable
136
+ def request_answer(agent:, workflow:, workflow_instance_id:, question:,
137
+ on_delivery_error:)
138
+ task = agent.invoke_async(question).map do |result|
139
+ {answer: result.fetch(:output)}
140
+ end
141
+
142
+ task.on_complete do |payload, operation_error|
143
+ delivery_error = nil
144
+ begin
145
+ accepted = workflow.signal(
146
+ workflow_instance_id: workflow_instance_id,
147
+ event: :answer_ready,
148
+ payload: operation_error ? {error: operation_error} : payload
149
+ )
150
+ unless accepted
151
+ delivery_error = Phronomy::Error.new("Workflow did not accept answer_ready")
152
+ end
153
+ rescue => error
154
+ delivery_error = error
155
+ end
156
+ on_delivery_error.call(delivery_error) if delivery_error
157
+ end
158
+
159
+ nil
160
+ end
161
+
162
+ class AnswerAgent < Phronomy::Agent::Base
163
+ agent_definition id: "recipe-answer", version: 1
164
+ model "gpt-4o-mini"
165
+ instructions "Answer briefly."
166
+ end
167
+
168
+ class AnswerState
169
+ include Phronomy::WorkflowContext
170
+ field :question, type: :replace
171
+ field :answer, type: :replace
172
+ field :error_message, type: :replace
173
+ end
174
+
175
+ agent = AnswerAgent.new
176
+ delivery_errors = Queue.new
177
+ workflow = nil
178
+ workflow = Phronomy::Workflow.define(AnswerState) do
179
+ initial :asking
180
+ state :asking
181
+ state :done
182
+ entry :asking, ->(state) {
183
+ request_answer(
184
+ agent: agent, workflow: workflow,
185
+ workflow_instance_id: state.workflow_instance_id, question: state.question,
186
+ on_delivery_error: ->(error) { delivery_errors << error }
187
+ )
188
+ state
189
+ }
190
+ transition from: :asking, on: :answer_ready, to: :done,
191
+ action: ->(state, event) {
192
+ error = event.payload[:error]
193
+ error ? state.merge(error_message: error.message) : state.merge(answer: event.payload.fetch(:answer))
194
+ }
195
+ transition from: :done, to: :__finish__
196
+ end
197
+
198
+ final = workflow.invoke(
199
+ {question: "What is a completion handle?"},
200
+ config: {workflow_instance_id: "recipe-answer"}
201
+ )
202
+ puts final.error_message || final.answer
203
+ raise delivery_errors.pop unless delivery_errors.empty?
204
+ ```
205
+
206
+ The receiver handles both answer and error; changing only the sender would be
207
+ incomplete. Store a serializable diagnostic, not the exception object, in the
208
+ Workflow context. Result mapping failures (such as a missing `:output`) also
209
+ become operation failures. Synchronous submission errors still propagate to
210
+ the caller of `request_answer`.
211
+
212
+ `signal == true` means admission, not transition completion or durable success.
213
+ `false` and signal exceptions reach `on_delivery_error` once without automatic
214
+ retry. This callback must be short and nonblocking. Its own exceptions follow
215
+ TaskResult's existing callback logging policy; they do not change the settled
216
+ operation result. Logging depends on the configured logger.
217
+
218
+ Delivery rejection does not itself settle a waiting Workflow. The application
219
+ must observe its diagnostic channel and decide whether to cancel or recover.
220
+ The synchronous invocation above illustrates successful delivery. Production
221
+ callers needing a wait limit should use `invoke_async` and manage both the
222
+ Workflow TaskResult and the diagnostic channel at their external boundary.
223
+
224
+ No generic Workflow forwarding helper is required by these recipes. A helper
225
+ must first establish how an old result is prevented from reaching a new waiting
226
+ stage; internal execution generations or automatic recovery are not implied.
227
+
228
+ For an application-local extraction with the same limited lifetime, see the
229
+ [bounded completion recipe](workflow-completion.md). It is not a new library API.
@@ -137,3 +137,11 @@ The published SVG is applied43-01. See the [naming boundary](entry-action-and-te
137
137
  and the separate [open Tool schema finding](tool-schema-recording-gap.md).
138
138
 
139
139
  Refactor 45 changes the token declarations as described in [the migration](rubyllm-2-token-ownership.md). Other Agent inheritance rules above remain unchanged. Tool schema/provider configuration follows the RubyLLM 2 inherited configuration contract.
140
+
141
+ ## Agent iteration budget correction
142
+
143
+ `Agent.max_iterations` now bounds logical LLM calls per Agent execution, including
144
+ its first call and its final response. The nonnegative Integer limit is captured
145
+ at admission; zero permits no call. Tool count, approval/resume and backend HTTP
146
+ retries do not consume additional iterations. Declaration inheritance is unchanged.
147
+ See [the iteration-budget boundary and migration](agent-iteration-budget.md).
@@ -0,0 +1,81 @@
1
+ # Agent iteration budget
2
+
3
+ ## Problem and meaning
4
+
5
+ Agent previously converted `max_iterations` to the generic FSM transition limit
6
+ `12 + 8 * max_iterations`. A single-Tool round currently takes ten transitions;
7
+ parallel Tool completions add further transitions. Default ten therefore stopped
8
+ the ninth Tool round before the tenth LLM call. Approval/recovery also creates new
9
+ FSM sessions, so a session counter cannot enforce an execution-wide Agent limit.
10
+
11
+ `Agent.max_iterations = N` now permits at most N logical LLM calls in one
12
+ AgentExecution, including the first and final call. Tool execution, authorization,
13
+ approval, streaming chunks and backend HTTP retries consume no additional calls.
14
+ This does not bound physical HTTP attempts, tokens, elapsed time or Tool count.
15
+
16
+ ## Owners and dependency direction
17
+
18
+ | Owner | Responsibility |
19
+ |---|---|
20
+ | Application / Agent declaration | Choose the maximum logical LLM calls. |
21
+ | Agent::IterationBudget | Validate the limit and decide whether another call is allowed from AgentExecution.llm_calls. |
22
+ | Agent preparation operations | Check before context hooks and provider dispatch; capture the limit in execution metadata. |
23
+ | Agent execution/recovery record encoders | Existing durable call identity, result recording and exactly-once recovery interpretation. |
24
+ | Agent::EngineSessionBuilder / InvocationTransitions | Select generic Engine safety mode and a bound appropriate to the Agent phase graph. |
25
+ | Engine::FSMSession | Bound automatic transition chains, without knowing LLMs, Tools, Agent records or iteration policy. |
26
+ | LLMAdapter / RubyLLM backend | Existing logical-call contract, wire transport, response handling and SDK retry policy. |
27
+
28
+ There is no new mutable Agent counter and no persistence access from Engine.
29
+ The call records already used for context call sequence and recovery remain the
30
+ single authority. InitialPreparation checks before the first call. DispatchPreparation
31
+ includes pending runtime outcomes through the existing record encoder, checks the
32
+ resulting call count, then prepares and commits the next provider dispatch.
33
+ Existing durable pending-call identity prevents unknown outcomes from being
34
+ automatically redispatched. No counter is incremented by Tool or Engine events.
35
+
36
+ Engine's optional `recursion_limit_scope: :automatic_chain` counts consecutive
37
+ accepted `state_completed` transitions. Only an accepted external transition
38
+ starts a new chain; consumed/stale events or events without a transition do not.
39
+ Agent selects this mode with its declared phase count as the safety bound.
40
+ Engine's default `:session` mode retains the previous total-transition limit for
41
+ Workflow and Tool sessions. Total step count remains available for diagnostics.
42
+ This is a generic mechanism, not an Engine dependency on Agent policy.
43
+
44
+ ADR-066's allocation remains intact: Agent owns execution and recovery, C09 owns
45
+ one model operation, C06 owns Tool execution, and the backend owns SDK details.
46
+ No RubyLLM, provider-specific or example-specific code is added.
47
+
48
+ ## Completion, suspension and compatibility
49
+
50
+ - The limit is a nonnegative Integer captured as `agent_max_iterations` in the
51
+ existing execution metadata at admission. Changing a class declaration during
52
+ approval cannot increase or reset the active run's budget. No DB schema migration.
53
+ - Resume/recovery uses the saved call records and saved limit. An older execution
54
+ without this metadata uses the current class limit and records it when preparing
55
+ another call. Its original limit cannot be reconstructed. Restart from legacy
56
+ initial preparation captures the fallback before input hooks.
57
+ - A new AgentExecution gets its own limit; handing work to another Agent gives
58
+ that Agent its own execution budget. GeneratorVerifier/SharedState orchestration
59
+ limits remain separate policies.
60
+ - A final response on call N succeeds. If call N requests Tools, those Tools
61
+ finish or await approval normally; the attempted next LLM call then fails with
62
+ the existing `Phronomy::RecursionLimitError`. Completed Tool results are retained;
63
+ no final answer or successful application outcome is fabricated.
64
+ - Zero dispatches no LLM call. Invalid limits fail before inference. Default ten
65
+ and declaration inheritance remain unchanged. Error class is retained; the
66
+ message now identifies the logical Agent limit rather than an FSM step limit.
67
+
68
+ This correction may allow more work than the old accidental FSM cutoff, or stop
69
+ an approval-heavy run previously able to reset its session counter. Deploy at a
70
+ quiescent Runtime boundary; do not hot-swap an active process. Application owners
71
+ must assess actual iteration needs after the counting bug is fixed. Increasing
72
+ example limits is not part of this change.
73
+
74
+ ## Verification scope
75
+
76
+ Regression tests cover nine Tool rounds plus final call ten, streaming, a real
77
+ RubyLLM retry through stubbed HTTP, many Tools in one response, exact cutoff,
78
+ fresh execution, approval and fresh Runtime restoration, recovered provider/Tool
79
+ outcomes, legacy metadata, zero and invalid limits. Generic Engine tests cover
80
+ automatic loops, stale events and unchanged default session limits. Live LM Studio
81
+ acceptance and its independent peg-native parsing failure remain separate gates.
@@ -0,0 +1,126 @@
1
+ # Agent scope and incarnation lifetime (DX05-E core)
2
+
3
+ An application decides how long it needs an Agent. C04 owns the mechanics for
4
+ acquiring that incarnation and releasing it safely. A scope does not delete saved
5
+ Agents, stop other Agents, close an application-owned Store, or infer whether a
6
+ business operation should be retried.
7
+
8
+ ## Application change
9
+
10
+ Before, a long-running application could create Agents for each request, but
11
+ the Runtime retained their mutable incarnations until shutdown. `close!` changes
12
+ the durable Agent lifecycle; `purge!` deletes saved state. Neither is a substitute
13
+ for releasing a process-local incarnation.
14
+
15
+ ```ruby
16
+ agent = ReviewAgent.create(persistence: store)
17
+ agent.invoke(text) # the Runtime still owns this incarnation afterwards
18
+ ```
19
+
20
+ After, the application declares the ownership boundary once:
21
+
22
+ ```ruby
23
+ Phronomy::Agent.open_scope do |agents|
24
+ agent = agents.create(ReviewAgent, persistence: store)
25
+ agent.invoke_async(text)
26
+ end
27
+ ```
28
+
29
+ `open_scope` waits for a returned TaskResult and returns its resolved value (or
30
+ raises the original exception). A plain block value is returned unchanged.
31
+ It then attempts to unload every Agent it acquired. A Workflow can use
32
+ `agents.create_async` or `agents.load_async` from EventLoop and compose their
33
+ TaskResults. The outer application keeps the scope alive until the Workflow has
34
+ finished. A scope is not serializable Workflow context.
35
+
36
+ An existing application-owned Agent cannot be adopted by `scope.load`: that
37
+ would give the scope permission to release someone else's object. The attempt
38
+ raises `AgentBusyError`. Repeated load in the same scope returns its live instance,
39
+ after checking class and Store compatibility. A listener cannot be replaced on
40
+ an already live instance. Existing `Agent.load`/`get` lookup semantics remain;
41
+ obtaining a reference does not extend its owning scope's lifetime.
42
+
43
+ ## Release contract
44
+
45
+ `agent.unload_async` returns `TaskResult<Boolean>` and is EventLoop-safe.
46
+ `agent.unload!(timeout: nil)` waits externally. Both preserve all durable state:
47
+ root and context revisions, journal position, contents, history, metadata and
48
+ lifecycle status. A closed Agent remains closed after reload.
49
+
50
+ Release requires an exact live owner, no accepted operations or execution
51
+ admission (including approval and recovery waits), and an idle durable execution
52
+ ledger with no retention references. C04 interprets only the presence of opaque
53
+ retention references; their owning domain decides when to remove them.
54
+
55
+ Concurrent release requests share the pending handle. A successful release
56
+ returns `true`; an already unloaded reference returns `false`. A wait timeout
57
+ does not cancel the release. Store check failure rolls back the reservation and
58
+ preserves the original error. Runtime shutdown closes admission and drains
59
+ already accepted transition completion without creating a replacement Runtime.
60
+
61
+ New operations cannot enter during release. Old Agent operations fail with
62
+ `AgentUnloadedError` (`code: "agent.unloaded"`, `agent_id`); asynchronous methods
63
+ return failed TaskResults. Immutable snapshots and `agent_id` remain usable.
64
+ A later load creates a new instance and can bind a new listener. The old
65
+ reference cannot mutate or release that new owner. Purged Agents keep their
66
+ existing `AgentPurgedError` semantics.
67
+
68
+ A concurrent `get` or `load` during release raises `AgentBusyError` immediately;
69
+ it does not wait or create a second owner. Accepted operations make unload fail
70
+ with Busy at its admission gate. In particular, unload inside an Agent terminal
71
+ listener is Busy while that listener is executing. Exit the outer application
72
+ scope after result delivery to release it. Existing immutable local-state reads
73
+ remain available while Runtime shutdown is draining an incomplete transition;
74
+ new execution and mutation admission stays closed.
75
+
76
+ ## Failure and cancellation
77
+
78
+ Scope tracks materialization receipts before starting work and registers acquired
79
+ Agents before returning them. Closure stops new acquisition and waits for the
80
+ physical completion of accepted materialization, even when its handle was
81
+ cancelled. Indeterminate durable create keeps the C04 recovery reservation and
82
+ logs the Agent ID; it is never silently made creatable again.
83
+
84
+ Scope does not cancel or wait for unjoined child execution as an implicit
85
+ cleanup policy. An active Agent is retained and cleanup reports `AgentBusyError`.
86
+ If both the main operation and cleanup fail, ScopeCleanupError retains the original
87
+ exception as primary_error/cause and every cleanup failure, plus the retryable scope.
88
+ If cleanup succeeds, the original exception is re-raised with the same identity.
89
+ If the main operation succeeded, failed cleanup raises `Agent::ScopeCleanupError`
90
+ (`code: "agent.scope_cleanup_failed"`). `result` preserves the successful value;
91
+ `failures` is an array of `{agent_id:, error:}` with the original exceptions.
92
+ All acquired Agents are attempted. No automatic retry, purge, or retention
93
+ removal is performed.
94
+
95
+ ## Responsibility and ordering
96
+
97
+ - C04 owns scope receipts, exact incarnation identity, operation admission,
98
+ busy/retention rules, invalidation and cleanup errors.
99
+ - C07 supplies the existing transaction boundary. C04's Agent Store supplies
100
+ `assert_idle!`, `guard_agent!` and retention reads; neither Persistence nor
101
+ physical Storage gains incarnation release policy.
102
+ - C13 supplies existing TaskResult and physical-completion contracts. It gains
103
+ no scope membership, Agent busy rules or unload states.
104
+ - Engine supplies existing worker execution, serial EventLoop delivery and
105
+ shutdown participation. It gains no Agent-specific states.
106
+ - The operation lease and release reservation share the C04 ownership mutex.
107
+ No Store transaction, Runtime call, application callback or blocking wait runs
108
+ under that mutex. Accepted runner continuations use C04 internal snapshots;
109
+ callbacks initiating a new public operation must acquire a new lease.
110
+ - Agent operation completion is distinct from cancelling its public handle.
111
+ C04 acknowledges release after domain terminal/listener delivery and physical
112
+ worker completion. A queued or physically running cancelled operation still
113
+ blocks release.
114
+
115
+ For notifications, finish child processing, drain notifications, then exit the
116
+ Agent scope. `run_once` retains its existing Store and lifetime behavior; merely
117
+ calling it does not opt into scope cleanup.
118
+
119
+ This is P5 (core). P6 migrates application examples and performs cross-repository
120
+ acceptance against the published or pinned candidate core.
121
+
122
+ ## Structured-output lifetime update
123
+
124
+ Scope now exposes `release!` and retains only failed owners for retry. CleanupError
125
+ also carries scope, resolved result and primary_error. The exact Task/error/release
126
+ rules are specified in [structured output and scope](structured-output-and-scope.md).