phronomy 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -1
- data/README.md +32 -6
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
- data/docs/architecture/agent-iteration-budget.md +81 -0
- data/docs/architecture/dx05-agent-scope.md +126 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
- data/docs/architecture/generator-verifier-ownership.md +18 -109
- data/docs/architecture/llm-continuation-metadata.md +88 -0
- data/docs/architecture/lm-studio-output-failures.md +111 -0
- data/docs/architecture/r8-unit15.md +3 -0
- data/docs/architecture/r8-unit7.md +2 -1
- data/docs/architecture/structured-output-and-scope.md +183 -0
- data/docs/architecture/tracing.md +22 -4
- data/docs/architecture/transport-error-boundaries.md +103 -0
- data/docs/architecture/trust-acceptance-followup.md +87 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +14 -2
- data/docs/getting-started.md +44 -43
- data/docs/migrations/r8-unit15.md +3 -0
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +10 -0
- data/lib/phronomy/agent/admission.rb +1 -1
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +121 -88
- data/lib/phronomy/agent/base.rb +82 -44
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/context_importer.rb +4 -0
- data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
- data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
- data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
- data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
- data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
- data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
- data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
- data/lib/phronomy/agent/execution_observation.rb +5 -5
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/output_contract.rb +64 -0
- data/lib/phronomy/agent/output_error.rb +53 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
- data/lib/phronomy/agent/runtime_input.rb +15 -4
- data/lib/phronomy/agent/saved_context_reader.rb +2 -1
- data/lib/phronomy/agent/store.rb +10 -2
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/common/values/immutable.rb +33 -0
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +18 -22
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/engine/fsm_session.rb +16 -1
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
- data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
- data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
- data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
- data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
- data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
- data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
- data/lib/phronomy/generation/generator_verifier.rb +170 -85
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
- data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
- data/lib/phronomy/llm_adapter/message.rb +4 -0
- data/lib/phronomy/llm_adapter/request.rb +17 -2
- data/lib/phronomy/llm_adapter/response.rb +11 -4
- data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/shared_state.rb +1 -0
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
- data/lib/phronomy/output_parser/parse_error.rb +10 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/automatic.rb +14 -8
- data/lib/phronomy/tracing/base.rb +37 -7
- data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
- data/lib/phronomy/tracing/observation.rb +2 -1
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +7 -2
- data/scripts/api_snapshot.rb +15 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/scripts/migrate_generator_verifier_result.rb +18 -0
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/execution_contract.rbs +4 -3
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +36 -5
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +8 -2
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/generator_verifier.rbs +70 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
- data/sig/phronomy/llm_adapter/values.rbs +8 -2
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/tracing/observation.rbs +1 -1
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +6 -0
- metadata +103 -5
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
- data/lib/phronomy/generation/low_confidence_error.rb +0 -14
- data/lib/phronomy/runtime_composition/generation_defaults.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46b13a1255f5821e321de0e3817f9fc2335ae2eba81dc7142a01295ffffcdd05
|
|
4
|
+
data.tar.gz: c268bfd441361880ce4c6c6a84f03e7e0530b5e1ca8736e87bfba78756a69c89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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).
|