phronomy 0.28.0 → 0.29.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/README.md +20 -5
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/dx05-agent-scope.md +119 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +4 -1
- data/docs/getting-started.md +44 -43
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +5 -0
- 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 +111 -87
- data/lib/phronomy/agent/base.rb +74 -43
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +11 -9
- data/lib/phronomy/agent/execution/execution_coordinator.rb +16 -13
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +1 -4
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +1 -4
- 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 +198 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +3 -3
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +7 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +5 -9
- data/lib/phronomy/agent/recovery/recovery_support.rb +2 -8
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +11 -18
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +5 -1
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- 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/low_confidence_error.rb +2 -2
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- 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/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/base.rb +35 -6
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
- 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 +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- 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/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +18 -0
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +1 -0
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
- data/sig/phronomy/integrations.rbs +14 -0
- 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/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +4 -0
- metadata +49 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Error API review (DX05-A / EA01–EA06)
|
|
2
|
+
|
|
3
|
+
Applications should branch on the meaning of a failed public operation without
|
|
4
|
+
knowing its backend or reconstructing it from an error message. The review uses
|
|
5
|
+
core `5b9d1047cc1351a935a7871ae7705c65166ea971` and examples
|
|
6
|
+
`5f4d58dc9432a84cb4421d3cc7dc2da92aee3c72` as its baseline.
|
|
7
|
+
|
|
8
|
+
The original DX05-A change implements the error API part of the follow-up design. DX05-B
|
|
9
|
+
(notification wiring), C (review result collection), D (`task_state`) and E
|
|
10
|
+
(Agent scope/unload) remain separate work. Their proposed APIs are not provided
|
|
11
|
+
by this patch. EA06's integration with D/E cannot be accepted before those APIs
|
|
12
|
+
exist. Live PostgreSQL, Redis, provider services and complete Rails execution
|
|
13
|
+
are separate acceptance gates; see the delivery's verification record.
|
|
14
|
+
|
|
15
|
+
The subsequent, unreleased DX05-D change now provides `task_state`; see
|
|
16
|
+
[its design and acceptance boundary](dx05-workflow-task-state.md).
|
|
17
|
+
Its error decision is included below. Agent scope/unload (DX05-E) remains
|
|
18
|
+
separate work. This does not change the historical DX05-A baseline above.
|
|
19
|
+
|
|
20
|
+
## Rules and ownership
|
|
21
|
+
|
|
22
|
+
* The owning Contract defines the failure category. A class is added only when
|
|
23
|
+
a caller has a distinct, stable decision to make. Detailed reasons use an
|
|
24
|
+
optional String `code`; they do not imply retry safety, rollback or worker exit.
|
|
25
|
+
* H00 owns the base Error and generic diagnostic projection/reconstruction.
|
|
26
|
+
It does not identify Agents, choose a retry policy or interpret domain codes.
|
|
27
|
+
* C07 Persistence and C12 Storage retain independent error hierarchies.
|
|
28
|
+
StorageBoundary translates only its existing known categories with an explicit
|
|
29
|
+
cause. A storage reason is not automatically a persistence reason.
|
|
30
|
+
* C13 transports the exception. It does not classify Agent, Team or Workflow
|
|
31
|
+
failures. `:cancelled` and a failed task carrying CancellationError remain
|
|
32
|
+
different facts. Partial `outcomes` remain on their existing execution errors.
|
|
33
|
+
* ArgumentError/TypeError describe Ruby/API input or mapping errors where already
|
|
34
|
+
documented. NotImplementedError marks an abstract extension point; it is not
|
|
35
|
+
converted into a provider failure.
|
|
36
|
+
* Existing message-only constructors remain valid. Seven public specialized
|
|
37
|
+
constructors now accept optional `code:` without changing their attributes.
|
|
38
|
+
New target-absence/busy constructors own fixed codes and object identities.
|
|
39
|
+
* Sync/async operation failures keep their category, reason and original cause.
|
|
40
|
+
Existing entry-time validation and submission rejection may still raise before
|
|
41
|
+
a TaskResult is returned. A TaskResult is not a promise of rollback.
|
|
42
|
+
|
|
43
|
+
## Operation decisions and acceptance
|
|
44
|
+
|
|
45
|
+
Each row names the operation family reviewed, its observed channel, the retained
|
|
46
|
+
asymmetry and the action. This is an evaluation of these operations, not a claim
|
|
47
|
+
that every possible backend failure has been injected.
|
|
48
|
+
|
|
49
|
+
| Owner | Public operations / failure meaning / channel | Decision and application effect | Acceptance evidence |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| C01 | TeamCoordinator.create/load: root absence, duplicate identity, uncertain create; direct raise | Add TeamNotFoundError only around the requested root load. Duplicate create retains Persistence conflict: a separate AlreadyExists class adds no necessary recovery choice here. A failed create readback keeps the original failure. | team_coordinator_spec; durable_coordination_spec |
|
|
52
|
+
| C01 | invoke/admission, TeamExecutionRepository.create_active/save/assert_idle!: active Team run | TeamBusyError with team_id and team.busy replaces AgentBusyError only for Team ownership. Child-Agent failures retain their own classes. No AgentBusy alias or inheritance. | execution_constraint_translation_spec; shared SQL coordination contract |
|
|
53
|
+
| C01 | HandoffRunner.invoke/cancel, Orchestrator fan-out, operation rejection | Retain ConfigurationError, HandoffError, StateConflictError, recovery requirements and on_error policy. Missing retained children are not missing Teams. Readback reconciliation may itself fail and does not prove absence. OperationRejected stays private; public code team.enqueue_after_finalize is unchanged. | durable_coordination_spec; integration/team_coordinator_spec; SQL reason diagnostics |
|
|
54
|
+
| C02 | Workflow definition/action, WorkflowContext writes, state CAS, waiting/resume | Retain ConfigurationError, WorkflowContextOwnershipError, WorkflowAsyncActionError and persistence conflict. Workflow has no equivalent target-root load API to justify another NotFound class. DX05-D adds WorkflowTaskResumeError with workflow.task_resume_unsupported only for unsupported restoration of a task-state phase, before any start side effect. Start/source/apply failures keep their original errors, and task cancellation remains distinct from a failed CancellationError. Agent scope is separate. | workflow and execution unit/integration suites; task_state_spec; shared SQL task-state contract (live PostgreSQL acceptance pending) |
|
|
55
|
+
| C03 | GeneratorVerifier.invoke; parse fallback and confidence decision | Preserve ParseError-only fallback and LowConfidenceError.result. Add optional constructor code; do not absorb provider/callback errors into a GenerationError. | generator_verifier specs; error_api_diagnostics_spec |
|
|
56
|
+
| C04 | Base.load, create/get, invoke/resume/purge | Add AgentNotFoundError only at the requested root. Journal/content/execution absence remains persistence failure. Preserve foreign AgentNotFound identity. AlreadyExists/Busy/Purged, uncertain purge and recovery-required retain their meaning. | error_api_boundaries_spec; Agent lifecycle/recovery suites |
|
|
57
|
+
| C04 | stream listener policy; result/failure persistence | StreamCallbackError keeps event_type/original_error/result, with optional code. Listener :fail_task versus :log_and_continue remains explicit; no unconditional callback swallowing. | stream_callback_error_policy_spec; error_api_diagnostics_spec |
|
|
58
|
+
| C05 | Assembly.prepare and policy invocation; plan/budget validation | Keep BudgetExceededError and ManifestError. Reuse C15 public trace_with, metadata only; a finish failure cannot mask policy failure or export the returned context. | error_api_boundaries_spec; Context suites |
|
|
59
|
+
| C06 | Tool.call / native async operation; schema, execution, async handle validity | Keep ToolError class; supply tool.invalid_arguments / tool.execution_failed / tool.invalid_async_result at owned conversion sites. Explicit async cause fixes delayed-failure information loss. Existing ToolError/cancellation pass through unchanged. :suppress survives logger failure. | tool/async_operation_spec; tool/base_spec; schema_error_spec |
|
|
60
|
+
| C07 | StorageBoundary, transaction, SaveOutcome, SnapshotComparison | Retain known-category mapping and original cause. Unknown I/O/application failures pass through. Keep committed/not_committed/unknown separately from exception class; no code-based reconciliation. | storage_boundary_spec; failure_contract_spec; snapshot_comparison_spec; SQL suites |
|
|
61
|
+
| C08 | fetch, fetch_text, fetch_json and content identity | Invalid requested JSON is IntegrityError with content.invalid_json and original ParserError; invalid UTF-8 is content.invalid_encoding. Raw fetch failures pass through; no dependency on OutputParser. Requesting JSON for plain text is also invalid representation, not proof of physical corruption. | error_api_boundaries_spec; content conformance; SQLite |
|
|
62
|
+
| C09 | LLM complete/stream, request/result checks, RubyLLM conversion | Keep Authentication/RateLimit/ContextLength as TransportError children and InvalidResult separate. Unknown exceptions, cancellation and callbacks retain identity. Existing SDK conversion is not an automatic retry policy. | LLMAdapter unit/integration suites |
|
|
63
|
+
| C10 | VectorStore add/search/remove/clear/size and AsyncClient | Keep InvalidResultError for invalid response and driver communication errors unchanged. Driver-specific transaction/effect uncertainty is not normalized without a backend-specific contract. No universal VectorStore TransportError or sibling-contract borrowing. | VectorStore and async-client suites; live PG/Redis pending |
|
|
64
|
+
| C11 | Embeddings.embed / AsyncClient; RubyLLM errors | Add owner-local AuthenticationError and RateLimitError under existing TransportError. Callers can request credentials versus surface quota refusal. Other SDK errors retain TransportError; arbitrary I/O failures, cancellation and programming errors are not guessed to be SDK failures. | error_api_boundaries_spec; embeddings/async_client_spec; RubyLLMEmbeddings specs |
|
|
65
|
+
| C12 | Records.read/fetch, conditions/unique constraints, streams/blobs, atomic views | Keep nil read versus raising fetch; preserve condition/resource/constraint. Add optional code to specialized constructors; SPI 2 and storage format unchanged. No Agent or Persistence semantics in the backend. | storage SPI snapshot; backend conformance; SQLite |
|
|
66
|
+
| C13 | TaskResult completion, map/flat_map/all_settled, wait/timeout/cancel | Transport errors unchanged. Observer failure leaves settled result intact and continues later observers even if logging fails. Transformation failure settles the derived task. Outcome attributes and physical completion remain separate. | task_result_spec; error_api_boundaries_spec; async composition suites |
|
|
67
|
+
| C15 | Base.trace, trace_with, OpenTelemetry.trace, Automatic | Explicit tracing propagates start/finish-only failure. After a primary business failure finish is attempted once and secondary report failure is suppressed. No finish after start failure. OpenTelemetry keeps with_span context. Automatic retains best-effort observation policy; Langfuse keeps its adapter-specific network suppression. | tracing/base_spec; open_telemetry_tracer_spec; automatic and extension specs |
|
|
68
|
+
| C16 | Filter.call/block!, PromptInjectionFilter | Keep explicit FilterBlockError and filter attribute; add optional constructor code. Implementation errors are not transformed into policy refusal. No unnecessary common rejection hierarchy. | filter specs; error_api_diagnostics_spec |
|
|
69
|
+
| C17 | JsonParser.parse, StructuredParser.parse | Keep ParseError for JSON and documented ArgumentError/TypeError schema-instantiation rejection. Other schema bugs pass through. User schemas should raise domain-specific errors when they must not be treated as mapping rejection. No blanket StandardError rescue added. | output_parser specs; schema integration |
|
|
70
|
+
| M91 | OrderedEventDelivery.publish/open/drain | ClosedError/OverflowError now inherit Phronomy::Error and report delivery.closed / delivery.capacity_exceeded. StandardError rescue still works; Phronomy::Error rescue now includes them. Reporting failure cannot replace an application error. No automatic resend. | ordered_event_delivery specs; error_api_boundaries_spec |
|
|
71
|
+
| I05/I06/I07/I09 | Concrete binding, AsyncClients, runtime admission/shutdown | Preserve backend-specific failures and execution admission/timeout categories. No new reverse references into domain contracts. | architecture gate; backend/execution integration |
|
|
72
|
+
| Other concrete helpers | Documents loaders, tools, evaluation and configuration | Preserve documented external operation errors and configuration checks; do not manufacture an application-wide ErrorTranslator. Generic Tool conversion applies when called through its public contract. | concrete tool/document/evaluation suites |
|
|
73
|
+
|
|
74
|
+
## Diagnostic format and recovery
|
|
75
|
+
|
|
76
|
+
Top-level `class`, `message` and optional `code` remain unchanged. An optional
|
|
77
|
+
`cause` object transports the same neutral fields. New cause data is bounded to
|
|
78
|
+
three ancestors and 2,048 characters per field. A further cause is represented
|
|
79
|
+
by `{ "truncated": true }`; an oversized code is omitted, never shortened into
|
|
80
|
+
another valid-looking code. Old top-level message behavior is unchanged.
|
|
81
|
+
Backtraces, arbitrary exception attributes and live objects are not serialized.
|
|
82
|
+
|
|
83
|
+
Agent and Team recovery use the same H00 decoder. It constructs only generic
|
|
84
|
+
Phronomy::Error values, including their optional causes, and never constantizes
|
|
85
|
+
saved names. Unknown/nil codes are retained. A restored cause is diagnostic data,
|
|
86
|
+
not the original exception object or its domain-specific attributes. Applications
|
|
87
|
+
may inspect `error.cause&.code` without depending on a storage exception class.
|
|
88
|
+
The live exception still retains its exact original cause.
|
|
89
|
+
|
|
90
|
+
## Migration and before/after use
|
|
91
|
+
|
|
92
|
+
Before, a broad Persistence/Storage NotFound rescue could mistake a broken
|
|
93
|
+
transcript for a missing Agent. After, limit creation to the requested identity:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
begin
|
|
97
|
+
agent = ConversationAgent.load(agent_id, persistence: store)
|
|
98
|
+
rescue Phronomy::AgentNotFoundError => error
|
|
99
|
+
raise unless error.agent_id == agent_id.to_s
|
|
100
|
+
agent = ConversationAgent.create(agent_id: agent_id, persistence: store)
|
|
101
|
+
end
|
|
102
|
+
# A later transcript failure is not handled as Agent absence.
|
|
103
|
+
messages = PhronomyExamples::TranscriptMessages.read(agent)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Before, an application handled a busy Team by rescuing AgentBusyError. After:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
begin
|
|
110
|
+
team.invoke(input)
|
|
111
|
+
rescue Phronomy::MultiAgent::TeamBusyError => error
|
|
112
|
+
# Application decides whether to expose an existing run or defer new work.
|
|
113
|
+
show_active_team(error.team_id)
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Before, Embeddings::TransportError alone required SDK knowledge to distinguish
|
|
118
|
+
credentials and quota. After, both remain catchable through TransportError:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
begin
|
|
122
|
+
vector = embeddings.embed(text)
|
|
123
|
+
rescue Phronomy::Embeddings::AuthenticationError
|
|
124
|
+
request_credentials
|
|
125
|
+
rescue Phronomy::Embeddings::RateLimitError
|
|
126
|
+
display_limit
|
|
127
|
+
rescue Phronomy::Embeddings::TransportError => error
|
|
128
|
+
report_provider_failure(error)
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
These are application choices, not core-provided UI or automatic retry helpers.
|
|
133
|
+
For Tool failures the application code need not change: `error.cause` now works
|
|
134
|
+
for late async failures too. New reason codes are additive; custom ToolError
|
|
135
|
+
instances pass through with their original nil/unknown code.
|
|
136
|
+
|
|
137
|
+
| Existing consumer | Migration / compatibility |
|
|
138
|
+
|---|---|
|
|
139
|
+
| Persistence::NotFoundError rescue around Agent/Team load | Still catches the new subclass; narrow it to the domain type and target ID for create-on-absence |
|
|
140
|
+
| Storage::NotFoundError rescue around Agent load | Already crossed the wrong boundary; update as the examples do |
|
|
141
|
+
| AgentBusyError rescue for Team's own admission | Change to MultiAgent::TeamBusyError; this is an intentional rescue-set change |
|
|
142
|
+
| JSON::ParserError rescue around content.fetch_json | Change to ContentStore::IntegrityError and inspect code; original ParserError is cause |
|
|
143
|
+
| Embeddings::TransportError rescue | Still catches both new subclasses |
|
|
144
|
+
| Phronomy::Error rescue around optional delivery | Now includes ClosedError/OverflowError; evaluate the application's handling |
|
|
145
|
+
| Exact equality of saved diagnostic hashes | Allow/test the optional cause object. Existing keys/codes and old JSON remain readable |
|
|
146
|
+
| Error message parsing or class-name constantization | Use public categories/codes for live failures; generic decoded diagnostics for recovery |
|
|
147
|
+
| Generic error logger retrying any error | Review separately; code/type does not prove rollback or idempotence |
|
|
148
|
+
|
|
149
|
+
## Dependency acceptance
|
|
150
|
+
|
|
151
|
+
No new Contract is introduced. New types live with C01/C04/C11; M91 remains an
|
|
152
|
+
optional consumer. C04/C01 specialize C07's absence category using existing
|
|
153
|
+
forward dependencies. H00 has no new domain reference. C05 uses C15's public
|
|
154
|
+
SPI wrapper rather than a private helper. C09/C10/C11 do not borrow each other's
|
|
155
|
+
error types. Ruby and RBS dependency graphs, not just Contract counts, are checked
|
|
156
|
+
by the existing architecture tool. See the generated audit for candidate tree
|
|
157
|
+
and exact deltas; the separate Contract diagram must describe the candidate as
|
|
158
|
+
unmerged until adoption is verified.
|
|
159
|
+
|
|
160
|
+
### Correction to the earlier Contract count
|
|
161
|
+
|
|
162
|
+
The baseline and candidate both have **32**, not 30, static Contract pairs.
|
|
163
|
+
The previous report omitted the existing RBS-only C01 → C08 references in
|
|
164
|
+
`sig/phronomy/multi_agent/store.rbs` (Store/_Records.contents) and C05 → C08
|
|
165
|
+
in `sig/phronomy/context.rbs` (Assembly.store). Both name the C08-owned
|
|
166
|
+
`_ContentRepository` interface. These are valid forward public-contract
|
|
167
|
+
references, not implementation dependencies introduced by this patch.
|
|
168
|
+
The candidate diagram restores these two edges and labels the correction.
|
|
169
|
+
|
|
170
|
+
At directory level the union still has 273 pairs, but two entries change:
|
|
171
|
+
M91 integrations → H00 common is added; multi_agent/persistence → agent is
|
|
172
|
+
removed because Team admission now owns its BusyError. Equal totals alone
|
|
173
|
+
would conceal this responsibility improvement. The 32 Contract pairs are
|
|
174
|
+
unchanged and acyclic; Ruby+RBS architecture validation passes.
|
|
175
|
+
|
|
176
|
+
## Static exception inventory
|
|
177
|
+
|
|
178
|
+
Ripper inspected 339 Ruby files. The following 63 definitions include base, private and optional-integration types; this is not a count of public APIs. Ruby declarations establish ownership. Existing RBS definitions and new owning-domain signatures are checked together.
|
|
179
|
+
|
|
180
|
+
| Class | Parent | Declaration |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `Phronomy::AgentAlreadyExistsError` | `Phronomy::Error` | `lib/phronomy/agent/agent_already_exists_error.rb` |
|
|
183
|
+
| `Phronomy::AgentBusyError` | `Phronomy::Error` | `lib/phronomy/agent/agent_busy_error.rb` |
|
|
184
|
+
| `Phronomy::AgentNotFoundError` | `Phronomy::Persistence::NotFoundError` | `lib/phronomy/agent/agent_not_found_error.rb` |
|
|
185
|
+
| `Phronomy::AgentPurgedError` | `Phronomy::Error` | `lib/phronomy/agent/agent_purged_error.rb` |
|
|
186
|
+
| `Phronomy::ExecutionRehydrationRequiredError` | `Phronomy::Error` | `lib/phronomy/agent/api/execution_rehydration_required_error.rb` |
|
|
187
|
+
| `Phronomy::HandoffError` | `Phronomy::Error` | `lib/phronomy/agent/handoff_error.rb` |
|
|
188
|
+
| `Phronomy::Agent::RecoveryCoordinator::ResolutionOutcomeUnknownError` | `Phronomy::Error` | `lib/phronomy/agent/recovery/recovery_coordinator.rb` |
|
|
189
|
+
| `Phronomy::StreamCallbackError` | `Phronomy::Error` | `lib/phronomy/agent/stream_callback_error.rb` |
|
|
190
|
+
| `Phronomy::ConfigurationError` | `Phronomy::Error` | `lib/phronomy/common/configuration_error.rb` |
|
|
191
|
+
| `Phronomy::Error` | `StandardError` | `lib/phronomy/common/error.rb` |
|
|
192
|
+
| `Phronomy::ContentStore::IntegrityError` | `Phronomy::Error` | `lib/phronomy/content_store/base.rb` |
|
|
193
|
+
| `Phronomy::Context::BudgetExceededError` | `Phronomy::Error` | `lib/phronomy/context/budget_exceeded_error.rb` |
|
|
194
|
+
| `Phronomy::Context::ManifestError` | `Phronomy::Error` | `lib/phronomy/context/manifest_error.rb` |
|
|
195
|
+
| `Phronomy::Embeddings::AuthenticationError` | `Phronomy::Embeddings::TransportError` | `lib/phronomy/embeddings/authentication_error.rb` |
|
|
196
|
+
| `Phronomy::Embeddings::InvalidResultError` | `Phronomy::Error` | `lib/phronomy/embeddings/invalid_result_error.rb` |
|
|
197
|
+
| `Phronomy::Embeddings::RateLimitError` | `Phronomy::Embeddings::TransportError` | `lib/phronomy/embeddings/rate_limit_error.rb` |
|
|
198
|
+
| `Phronomy::Embeddings::TransportError` | `Phronomy::Error` | `lib/phronomy/embeddings/transport_error.rb` |
|
|
199
|
+
| `Phronomy::BackpressureError` | `Phronomy::Error` | `lib/phronomy/execution/backpressure_error.rb` |
|
|
200
|
+
| `Phronomy::CancellationError` | `Phronomy::Error` | `lib/phronomy/execution/cancellation_error.rb` |
|
|
201
|
+
| `Phronomy::EventLoopReentrancyError` | `Phronomy::Error` | `lib/phronomy/execution/event_loop_reentrancy_error.rb` |
|
|
202
|
+
| `Phronomy::ExecutionCancellationError` | `Phronomy::CancellationError` | `lib/phronomy/execution/execution_cancellation_error.rb` |
|
|
203
|
+
| `Phronomy::ExecutionTimeoutError` | `Phronomy::TimeoutError` | `lib/phronomy/execution/execution_timeout_error.rb` |
|
|
204
|
+
| `Phronomy::InvalidAsyncEntryActionError` | `Phronomy::InvalidAsyncWorkflowActionError` | `lib/phronomy/execution/invalid_async_entry_action_error.rb` |
|
|
205
|
+
| `Phronomy::InvalidAsyncTransitionActionError` | `Phronomy::InvalidAsyncWorkflowActionError` | `lib/phronomy/execution/invalid_async_transition_action_error.rb` |
|
|
206
|
+
| `Phronomy::InvalidAsyncWorkflowActionError` | `Phronomy::Error` | `lib/phronomy/execution/invalid_async_workflow_action_error.rb` |
|
|
207
|
+
| `Phronomy::PoolShutdownError` | `Phronomy::Error` | `lib/phronomy/execution/pool_shutdown_error.rb` |
|
|
208
|
+
| `Phronomy::RecursionLimitError` | `Phronomy::Error` | `lib/phronomy/execution/recursion_limit_error.rb` |
|
|
209
|
+
| `Phronomy::RuntimeShutdownError` | `Phronomy::Error` | `lib/phronomy/execution/runtime_shutdown_error.rb` |
|
|
210
|
+
| `Phronomy::RuntimeShutdownReentrancyError` | `Phronomy::RuntimeShutdownError` | `lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb` |
|
|
211
|
+
| `Phronomy::SchedulerReentrancyError` | `Phronomy::EventLoopReentrancyError` | `lib/phronomy/execution/scheduler_reentrancy_error.rb` |
|
|
212
|
+
| `Phronomy::TimeoutError` | `Phronomy::Error` | `lib/phronomy/execution/timeout_error.rb` |
|
|
213
|
+
| `Phronomy::FilterBlockError` | `Phronomy::Error` | `lib/phronomy/filter/filter_block_error.rb` |
|
|
214
|
+
| `Phronomy::LowConfidenceError` | `Phronomy::Error` | `lib/phronomy/generation/low_confidence_error.rb` |
|
|
215
|
+
| `Phronomy::Integrations::OrderedEventDelivery::ClosedError` | `Phronomy::Error` | `lib/phronomy/integrations/ordered_event_delivery.rb` |
|
|
216
|
+
| `Phronomy::Integrations::OrderedEventDelivery::OverflowError` | `Phronomy::Error` | `lib/phronomy/integrations/ordered_event_delivery.rb` |
|
|
217
|
+
| `Phronomy::LLMAdapter::AuthenticationError` | `Phronomy::LLMAdapter::TransportError` | `lib/phronomy/llm_adapter/authentication_error.rb` |
|
|
218
|
+
| `Phronomy::LLMAdapter::RubyLLM::ToolsRequested` | `StandardError` | `lib/phronomy/llm_adapter/backends/ruby_llm.rb` |
|
|
219
|
+
| `Phronomy::LLMAdapter::ContextLengthError` | `Phronomy::Error` | `lib/phronomy/llm_adapter/context_length_error.rb` |
|
|
220
|
+
| `Phronomy::LLMAdapter::InvalidResultError` | `Phronomy::Error` | `lib/phronomy/llm_adapter/invalid_result_error.rb` |
|
|
221
|
+
| `Phronomy::LLMAdapter::RateLimitError` | `Phronomy::LLMAdapter::TransportError` | `lib/phronomy/llm_adapter/rate_limit_error.rb` |
|
|
222
|
+
| `Phronomy::LLMAdapter::TransportError` | `Phronomy::Error` | `lib/phronomy/llm_adapter/transport_error.rb` |
|
|
223
|
+
| `Phronomy::MultiAgent::TeamBusyError` | `Phronomy::Error` | `lib/phronomy/multi_agent/team_busy_error.rb` |
|
|
224
|
+
| `Phronomy::MultiAgent::TeamCoordinator::OperationRejected` | `Phronomy::ConfigurationError` | `lib/phronomy/multi_agent/team_coordinator.rb` |
|
|
225
|
+
| `Phronomy::MultiAgent::TeamNotFoundError` | `Phronomy::Persistence::NotFoundError` | `lib/phronomy/multi_agent/team_not_found_error.rb` |
|
|
226
|
+
| `Phronomy::ParseError` | `Phronomy::Error` | `lib/phronomy/output_parser/parse_error.rb` |
|
|
227
|
+
| `Phronomy::Persistence::Error` | `Phronomy::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
228
|
+
| `Phronomy::Persistence::ConflictError` | `Phronomy::Persistence::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
229
|
+
| `Phronomy::Persistence::StateConflictError` | `Phronomy::Persistence::ConflictError` | `lib/phronomy/persistence/errors.rb` |
|
|
230
|
+
| `Phronomy::Persistence::NotFoundError` | `Phronomy::Persistence::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
231
|
+
| `Phronomy::Persistence::SerializationError` | `Phronomy::Persistence::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
232
|
+
| `Phronomy::Persistence::UnsupportedBackendError` | `Phronomy::Persistence::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
233
|
+
| `Phronomy::Persistence::TransactionError` | `Phronomy::Persistence::Error` | `lib/phronomy/persistence/errors.rb` |
|
|
234
|
+
| `Phronomy::Storage::BlobConflictError` | `Phronomy::Storage::ConflictError` | `lib/phronomy/storage/blob_conflict_error.rb` |
|
|
235
|
+
| `Phronomy::Storage::ConditionFailedError` | `Phronomy::Storage::ConflictError` | `lib/phronomy/storage/condition_failed_error.rb` |
|
|
236
|
+
| `Phronomy::Storage::ConflictError` | `Phronomy::Error` | `lib/phronomy/storage/conflict_error.rb` |
|
|
237
|
+
| `Phronomy::Storage::NotFoundError` | `Phronomy::Error` | `lib/phronomy/storage/not_found_error.rb` |
|
|
238
|
+
| `Phronomy::Storage::SerializationError` | `Phronomy::Error` | `lib/phronomy/storage/serialization_error.rb` |
|
|
239
|
+
| `Phronomy::Storage::TransactionError` | `Phronomy::Error` | `lib/phronomy/storage/transaction_error.rb` |
|
|
240
|
+
| `Phronomy::Storage::UniqueConstraintError` | `Phronomy::Storage::ConflictError` | `lib/phronomy/storage/unique_constraint_error.rb` |
|
|
241
|
+
| `Phronomy::Storage::UnsupportedBackendError` | `Phronomy::Error` | `lib/phronomy/storage/unsupported_backend_error.rb` |
|
|
242
|
+
| `Phronomy::ToolError` | `Phronomy::Error` | `lib/phronomy/tool/tool_error.rb` |
|
|
243
|
+
| `Phronomy::VectorStore::InvalidResultError` | `Phronomy::Error` | `lib/phronomy/vector_store/invalid_result_error.rb` |
|
|
244
|
+
| `Phronomy::WorkflowContextOwnershipError` | `Phronomy::Error` | `lib/phronomy/workflow/execution/workflow_context_ownership_error.rb` |
|
|
245
|
+
| `Phronomy::WorkflowTaskResumeError` (DX05-D) | `Phronomy::Error` | `lib/phronomy/workflow/execution/workflow_task_resume_error.rb` |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Failure reason codes
|
|
2
|
+
|
|
3
|
+
`Phronomy::Error` and its subclasses accept an optional `code:` String and expose
|
|
4
|
+
it through `error.code`. Unspecified codes are nil. Supplied strings are copied
|
|
5
|
+
and frozen. Existing message, class, cause and backtrace behavior is retained.
|
|
6
|
+
No change is made to StandardError or third-party exceptions.
|
|
7
|
+
|
|
8
|
+
This is an additive public-contract extension. `class` and `message` remain in
|
|
9
|
+
saved failure diagnostics. When a Phronomy exception has a code, its diagnostic
|
|
10
|
+
also contains `"code"`. The same generic field is retained through Agent failure
|
|
11
|
+
persistence, Team results, and terminal resume/reboot. A code is not an execution
|
|
12
|
+
status and is not a retry permission.
|
|
13
|
+
|
|
14
|
+
| Code | Owner and meaning |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `team.enqueue_after_finalize` | MultiAgent: a new enqueue was conclusively rejected after finalization, following the existing readback checks |
|
|
17
|
+
|
|
18
|
+
The operation wrapper remains catchable as ConfigurationError. A Team's aggregate
|
|
19
|
+
failure remains a Phronomy::Error and now carries the stored code. An operation
|
|
20
|
+
that already succeeded with the same identity still replays its original result.
|
|
21
|
+
Unknown persistence outcomes and readback failures remain recovery requirements;
|
|
22
|
+
an inner code is not blindly copied onto that outer uncertainty.
|
|
23
|
+
|
|
24
|
+
The application may choose to retry only a durably failed planning attempt:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
outcome && outcome[:status] == "failed" &&
|
|
28
|
+
outcome.dig(:error, "code") == "team.enqueue_after_finalize"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Example 21 retains its four-section/content-length validator and existing retry
|
|
32
|
+
limit. That application policy is not part of the reason code contract.
|
|
33
|
+
|
|
34
|
+
## Compatibility
|
|
35
|
+
|
|
36
|
+
No SQL columns or record-format versions change. Error diagnostics are content
|
|
37
|
+
JSON; the existing content codec carries the optional field without interpreting
|
|
38
|
+
it. Old diagnostics without codes remain readable. Unknown string codes are
|
|
39
|
+
preserved, and no code is inferred from a message.
|
|
40
|
+
|
|
41
|
+
A v0.28.0 reader can inspect the extra JSON field through stored result data.
|
|
42
|
+
Its reconstructed exceptions do not expose the new code, and paths that rebuild
|
|
43
|
+
a failure from class/message can lose it. Mixed-version writes and code-based
|
|
44
|
+
exception handling on an old reader are therefore unsupported. Upgrade consumers
|
|
45
|
+
before relying on codes; there is no automatic historical backfill.
|
data/docs/features.md
CHANGED
|
@@ -30,6 +30,7 @@ for production deployments.
|
|
|
30
30
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
31
31
|
| **Tracing** — Pluggable logical-operation span observability with bounded automatic coverage | Stable |
|
|
32
32
|
| **Error Taxonomy** — Provider errors translated to Phronomy transport/authentication/rate-limit/context errors | Beta |
|
|
33
|
+
| **Optional failure reason codes** — `Phronomy::Error#code` and `code:` retain domain-supplied reasons in saved Agent/Team diagnostics; [meaning and compatibility](failure-reason-codes.md) | Beta |
|
|
33
34
|
|
|
34
35
|
Agent definition lineage is separate from Agent instance identity. A named
|
|
35
36
|
concrete Agent may declare `agent_definition version: N`; when `id:` is omitted,
|
|
@@ -51,6 +52,7 @@ rather than implicitly inheriting the parent revision. The Stable
|
|
|
51
52
|
| **VectorStore::Embeddings::AsyncClient** — `embed_async` offloads synchronous `embed` through Phronomy and returns `TaskResult` | Beta |
|
|
52
53
|
| **Model Context Protocol (MCP) Tool** — `Phronomy::Tools::Mcp` integration through the official `mcp` gem | Beta |
|
|
53
54
|
| **Agent Tool** — `Phronomy::Tools::Agent.from_agent` exposes a child Agent as a Tool without occupying a worker while waiting | Beta |
|
|
55
|
+
| **Ordered event delivery** — Explicitly require `Phronomy::Integrations::OrderedEventDelivery` for bounded ordered delivery outside lifecycle callbacks; [lifetime and failure rules](ordered-event-delivery.md) | Beta |
|
|
54
56
|
| **Vector Search Tool** — `Phronomy::Tools::VectorSearch` wraps VectorStore and Embeddings adapters | Beta |
|
|
55
57
|
|
|
56
58
|
Applications previously calling async methods on backend objects must
|
|
@@ -63,6 +65,7 @@ Backend authors continue to implement the synchronous SPI only.
|
|
|
63
65
|
|---|---|
|
|
64
66
|
| **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions; single writer for Phronomy-managed live Agent execution state | Beta |
|
|
65
67
|
| **Agent execution result authority** — Session-local routing plus current FSM state and purpose-specific semantic IDs (`llm_call_id`, `tool_invocation_id`) reject stale async results | Beta |
|
|
68
|
+
| **Agent scope and unload** — `Agent.open_scope` owns newly acquired Agents; `unload_async` / `unload!` release idle incarnations without changing saved state, with typed invalidation and cleanup failures; [contract](architecture/dx05-agent-scope.md) | Beta |
|
|
66
69
|
| **Agent live ownership/admission** — Runtime owns one mutable live Agent per `agent_id`; EventLoop rejects competing same-Agent top-level executions while suspension retains the logical execution slot; Persistence admission remains durable defense | Beta |
|
|
67
70
|
| **Workflow durable admission** — Same-process admission is keyed by durable `workflow_instance_id` from pre-hydration admission through the durable terminal/halted save barrier; admission-owner representation is Runtime-internal | Beta |
|
|
68
71
|
| **`invoke` / `invoke_async`** — Blocking and non-blocking Agent/Workflow entry points | Stable |
|
|
@@ -92,7 +95,7 @@ See the [migration guide](migrations/parallel-tool-chat-removal.md).
|
|
|
92
95
|
|
|
93
96
|
| Feature | Stability |
|
|
94
97
|
|---|---|
|
|
95
|
-
| **Workflow asynchronous pattern** —
|
|
98
|
+
| **Workflow asynchronous pattern** — `task_state` starts a `TaskResult`, applies its successful value once to the current state entry, and then evaluates ordinary transitions; `Workflow#signal` remains the application event API | Beta |
|
|
96
99
|
| **Durable Handoff** — `MultiAgent::Handoff` and `MultiAgent::HandoffRunner` persist responsibility, immutable Context and exact Target reservation in one Persistence domain; current graph wiring is required for continuation | Beta |
|
|
97
100
|
| **GeneratorVerifier** — Generator-Verifier loop with injectable prompts/parsers | Beta |
|
|
98
101
|
| **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
data/docs/getting-started.md
CHANGED
|
@@ -4,6 +4,11 @@ This guide contains the setup and introductory examples that were previously
|
|
|
4
4
|
embedded in the repository README. The README remains the project entry point;
|
|
5
5
|
this document carries the longer operational examples.
|
|
6
6
|
|
|
7
|
+
Blocks marked `ruby runnable` are complete after `require "phronomy"` and provider
|
|
8
|
+
configuration. Other blocks are setup or partial examples that reuse definitions
|
|
9
|
+
from the surrounding section. See [application recipes](application-recipes.md)
|
|
10
|
+
for independent examples and explicit failure handling.
|
|
11
|
+
|
|
7
12
|
## Install
|
|
8
13
|
|
|
9
14
|
Add Phronomy to your Gemfile:
|
|
@@ -49,11 +54,9 @@ Install only the backend gems required by your application:
|
|
|
49
54
|
|
|
50
55
|
## Define a Tool and Agent
|
|
51
56
|
|
|
52
|
-
Use `Phronomy::Tool::Base` as the application-facing authoring API.
|
|
53
|
-
exact alias of the existing `Phronomy::Tool::Base`, so
|
|
54
|
-
existing Tool definitions using the longer namespace remain compatible.
|
|
57
|
+
Use `Phronomy::Tool::Base` as the application-facing authoring API.
|
|
55
58
|
|
|
56
|
-
```ruby
|
|
59
|
+
```ruby runnable
|
|
57
60
|
class WebSearch < Phronomy::Tool::Base
|
|
58
61
|
description "Search the web"
|
|
59
62
|
param :query, type: :string, desc: "Search query"
|
|
@@ -115,7 +118,7 @@ A Runtime has at most one mutable live Agent object for an `agent_id`. If the
|
|
|
115
118
|
Agent is already live, repeated `load` returns that exact Ruby object and does
|
|
116
119
|
not reload Persistence. If it is not live, `load` hydrates the durable Agent
|
|
117
120
|
once. Loading an ID that does not exist durably raises
|
|
118
|
-
`
|
|
121
|
+
`Phronomy::Persistence::NotFoundError`.
|
|
119
122
|
|
|
120
123
|
Use `get` when only a process-local lookup is wanted:
|
|
121
124
|
|
|
@@ -131,7 +134,7 @@ While the Agent is live, it owns its current AgentRoot/Journal view and
|
|
|
131
134
|
Runtime/EventLoop owns process-local identity/admission/execution authority.
|
|
132
135
|
Phronomy persists snapshots at defined durability boundaries but does not reload
|
|
133
136
|
mutable Agent/Execution/Journal state before every LLM or Tool step. A conflicting
|
|
134
|
-
external durable write is surfaced as `
|
|
137
|
+
external durable write is surfaced as `Phronomy::Persistence::StateConflictError` rather than
|
|
135
138
|
silently merged into the live instance.
|
|
136
139
|
|
|
137
140
|
The active transcript and Knowledge views can be advanced independently without
|
|
@@ -165,7 +168,7 @@ the listener when the Agent is materialized, not on each invocation:
|
|
|
165
168
|
|
|
166
169
|
```ruby
|
|
167
170
|
agent = ResearchAgent.load(
|
|
168
|
-
"
|
|
171
|
+
"another-session-not-live-in-this-runtime",
|
|
169
172
|
persistence: persistence,
|
|
170
173
|
on_event: ->(event) {
|
|
171
174
|
puts event.payload[:output] if event.type == :done
|
|
@@ -175,6 +178,10 @@ agent = ResearchAgent.load(
|
|
|
175
178
|
task = agent.invoke_async("Hello")
|
|
176
179
|
```
|
|
177
180
|
|
|
181
|
+
This load assumes that the other Agent exists in Persistence but has not been
|
|
182
|
+
materialized in this Runtime. To listen to `research-session-42`, register its
|
|
183
|
+
listener in the original `create` call.
|
|
184
|
+
|
|
178
185
|
`new`, `create`, and `load` also accept an equivalent listener block.
|
|
179
186
|
Supplying both `on_event:` and a construction block is an error. If `load`
|
|
180
187
|
resolves an already-live same-process Agent, supplying any new listener/block
|
|
@@ -299,18 +306,16 @@ not inject an explicit `persistence:`. Workflow durability is fixed at the
|
|
|
299
306
|
application/Workflow-definition boundary; there is no per-invocation backend
|
|
300
307
|
switch.
|
|
301
308
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
Use `task_state` when a state starts an asynchronous operation and needs its
|
|
310
|
+
terminal result before proceeding. `start` returns the TaskResult immediately;
|
|
311
|
+
`apply` updates the current Workflow context on EventLoop. Guards run after
|
|
312
|
+
successful application. Ordinary entry and transition actions remain synchronous.
|
|
306
313
|
|
|
307
|
-
```ruby
|
|
314
|
+
```ruby runnable
|
|
308
315
|
class AnswerContext
|
|
309
316
|
include Phronomy::WorkflowContext
|
|
310
|
-
|
|
311
317
|
field :question, type: :replace, default: ""
|
|
312
|
-
field :answer,
|
|
313
|
-
# workflow_instance_id is framework-owned metadata; do not declare it as a field.
|
|
318
|
+
field :answer, type: :replace, default: nil
|
|
314
319
|
end
|
|
315
320
|
|
|
316
321
|
class ResearchAgent < Phronomy::Agent::Base
|
|
@@ -320,40 +325,35 @@ class ResearchAgent < Phronomy::Agent::Base
|
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
my_agent = ResearchAgent.new
|
|
323
|
-
workflow = nil
|
|
324
|
-
|
|
325
328
|
workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
326
329
|
initial :asking
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
task_state :asking,
|
|
331
|
+
start: ->(state) { my_agent.invoke_async(state.question) },
|
|
332
|
+
apply: ->(state, result) { state.merge(answer: result.fetch(:output)) }
|
|
333
|
+
transition from: :asking, to: :__finish__
|
|
334
|
+
end
|
|
332
335
|
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
answer = workflow.invoke(
|
|
337
|
+
{question: "What is an asynchronous completion handle?"},
|
|
338
|
+
config: {workflow_instance_id: "answer-example"}
|
|
339
|
+
)
|
|
340
|
+
puts answer.answer
|
|
341
|
+
```
|
|
335
342
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
end
|
|
343
|
+
Phronomy correlates completion to this execution and state entry, ignores stale
|
|
344
|
+
results after exit or reentry, and propagates start/source/apply failures to the
|
|
345
|
+
caller. A cancelled source cancels the Workflow observation; a failed source
|
|
346
|
+
retains its failure classification. External business events still use
|
|
347
|
+
`Workflow#signal` or `send_event`.
|
|
342
348
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
to: :done,
|
|
350
|
-
action: ->(ctx, event) { ctx.merge(answer: event.payload[:answer]) }
|
|
351
|
-
)
|
|
352
|
-
end
|
|
353
|
-
```
|
|
349
|
+
`task_state` is a live wait, while `wait_state` halts the current execution segment.
|
|
350
|
+
Pending Tasks are not persisted: attempting to restore a task-state snapshot
|
|
351
|
+
raises `WorkflowTaskResumeError`. Normal wait-state halt/resume remains supported.
|
|
352
|
+
Parent cancellation stops application of the result; cancelling the child itself
|
|
353
|
+
requires explicit controls passed by the application. See the
|
|
354
|
+
[task-state contract](architecture/dx05-workflow-task-state.md) for these boundaries.
|
|
354
355
|
|
|
355
|
-
Returning a
|
|
356
|
-
implicit await mechanism and is rejected.
|
|
356
|
+
Returning a TaskResult from an ordinary entry/transition action remains an error.
|
|
357
357
|
|
|
358
358
|
## Agent as Tool
|
|
359
359
|
|
|
@@ -435,6 +435,7 @@ contracts.
|
|
|
435
435
|
## Next steps
|
|
436
436
|
|
|
437
437
|
- [Features and API stability](features.md)
|
|
438
|
+
- [Application recipes](application-recipes.md)
|
|
438
439
|
- [Runtime and concurrency](runtime-and-concurrency.md)
|
|
439
440
|
- [Architecture decisions](decisions/)
|
|
440
441
|
- [Migration guides](migrations/)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Ordered event delivery
|
|
2
|
+
|
|
3
|
+
Load the application integration explicitly:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require "phronomy"
|
|
7
|
+
require "phronomy/integrations/ordered_event_delivery"
|
|
8
|
+
|
|
9
|
+
Phronomy::Integrations::OrderedEventDelivery.open(
|
|
10
|
+
capacity: 256, batch_size: 32, flush_timeout: 30,
|
|
11
|
+
deliver: ->(payload) { send_to_application_transport(payload) }
|
|
12
|
+
) do |delivery|
|
|
13
|
+
agent = MyAgent.new(on_event: ->(event) {
|
|
14
|
+
payload = application_payload(event)
|
|
15
|
+
delivery.publish(payload) if payload
|
|
16
|
+
})
|
|
17
|
+
agent.stream("Hello")
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The block runs on the external caller, and its result is returned after drain.
|
|
22
|
+
`open` rejects EventLoop callers before executing their block. Publication only
|
|
23
|
+
snapshots and queues plain data; it does not wait for capacity or send network
|
|
24
|
+
traffic. The existing Blocking API supplies a worker only while draining. The
|
|
25
|
+
integration creates no scheduler and never shuts down the shared Runtime/pool.
|
|
26
|
+
|
|
27
|
+
The app owns authentication, destination, authorization, Rails executor wrapping,
|
|
28
|
+
event-to-payload conversion, and any token coalescing. `prepare_batch:` optionally
|
|
29
|
+
receives an immutable Array of snapshots on the drain worker and returns an Array
|
|
30
|
+
of payloads to send in order. Its default is no transformation. Example 18 passes
|
|
31
|
+
its own TokenEventBatch, preserving adjacent-token coalescing with equal metadata.
|
|
32
|
+
|
|
33
|
+
Capacity limits the waiting queue, excluding an already extracted batch. Multiple
|
|
34
|
+
publishers are ordered by queue admission, not by thread start time. Hashes,
|
|
35
|
+
Arrays, Strings and scalar plain values are copied into immutable snapshots;
|
|
36
|
+
cycles, non-finite numbers and arbitrary objects fail publication.
|
|
37
|
+
|
|
38
|
+
On overflow/invalid publication, acceptance closes and the failure remains visible
|
|
39
|
+
to final drain even if an Agent listener merely logs its exception. Previously
|
|
40
|
+
accepted work drains if transport is healthy. A failed batch conversion, delivery
|
|
41
|
+
or worker admission also closes acceptance; remaining work may not be delivered.
|
|
42
|
+
There is no silent drop-as-success, automatic retry or durable delivery guarantee.
|
|
43
|
+
|
|
44
|
+
`close_and_wait` can be called by multiple external threads. It waits for physical
|
|
45
|
+
drain completion, not just TaskResult settlement. A timeout ends only the wait;
|
|
46
|
+
a network operation can still complete afterwards. Configure transport timeouts
|
|
47
|
+
in the transport itself. Network failure does not prove that nothing was sent.
|
|
48
|
+
|
|
49
|
+
If application execution fails, its original exception, message and backtrace
|
|
50
|
+
remain primary after cleanup. A simultaneous delivery failure is reported through
|
|
51
|
+
the configured Phronomy logger. Without a logger there is no durable diagnostic
|
|
52
|
+
guarantee; logger failure falls back to stderr without replacing the primary error.
|
|
53
|
+
If only delivery fails, that failure is raised. The small final-answer-only Job
|
|
54
|
+
needs no queue and is unchanged.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# A bounded Workflow completion recipe
|
|
2
|
+
|
|
3
|
+
This is a **legacy manual-signal recipe**, retained for compatibility and its
|
|
4
|
+
bounded-lifetime tests. New code should use
|
|
5
|
+
[`task_state`](architecture/dx05-workflow-task-state.md), which owns completion
|
|
6
|
+
correlation and failure propagation for operations started by a Workflow state.
|
|
7
|
+
The helper below is application code and is not a Phronomy integration API.
|
|
8
|
+
Its limitations do not describe the new task-state API.
|
|
9
|
+
|
|
10
|
+
Use the application-local function below only for **one live waiting stage, with
|
|
11
|
+
no restart, reentry or replacement while its operation is pending**. It separates
|
|
12
|
+
result mapping (application logic) from the repetitive notification plumbing.
|
|
13
|
+
For repeated/resumed stages, the app must put an operation ID in every success
|
|
14
|
+
AND error payload and check it with a receiver-side transition `guard:` before
|
|
15
|
+
applying the payload. Signal admission alone is not that check. The example
|
|
16
|
+
below intentionally does not claim that wider guarantee.
|
|
17
|
+
|
|
18
|
+
```ruby runnable
|
|
19
|
+
def forward_completion(task, workflow:, workflow_instance_id:, event:, on_delivery_error:)
|
|
20
|
+
task.on_complete do |payload, operation_error|
|
|
21
|
+
delivery_error = nil
|
|
22
|
+
begin
|
|
23
|
+
accepted = workflow.signal(
|
|
24
|
+
workflow_instance_id: workflow_instance_id, event: event,
|
|
25
|
+
payload: operation_error ? {error: operation_error} : payload
|
|
26
|
+
)
|
|
27
|
+
delivery_error = Phronomy::Error.new("Workflow did not accept #{event}") unless accepted
|
|
28
|
+
rescue => error
|
|
29
|
+
delivery_error = error
|
|
30
|
+
end
|
|
31
|
+
on_delivery_error.call(delivery_error) if delivery_error
|
|
32
|
+
end
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class RecipeAnswerAgent < Phronomy::Agent::Base
|
|
37
|
+
agent_definition id: "bounded-completion-recipe", version: 1
|
|
38
|
+
model "gpt-4o-mini"
|
|
39
|
+
instructions "Answer briefly."
|
|
40
|
+
end
|
|
41
|
+
class CompletionState
|
|
42
|
+
include Phronomy::WorkflowContext
|
|
43
|
+
field :question, type: :replace
|
|
44
|
+
field :answer, type: :replace
|
|
45
|
+
field :error_message, type: :replace
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
agent = RecipeAnswerAgent.new
|
|
49
|
+
errors = Queue.new
|
|
50
|
+
workflow = nil
|
|
51
|
+
workflow = Phronomy::Workflow.define(CompletionState) do
|
|
52
|
+
initial :asking
|
|
53
|
+
state :asking
|
|
54
|
+
state :done
|
|
55
|
+
entry :asking, ->(state) {
|
|
56
|
+
task = agent.invoke_async(state.question).map { |result| {answer: result.fetch(:output)} }
|
|
57
|
+
forward_completion(task, workflow: workflow,
|
|
58
|
+
workflow_instance_id: state.workflow_instance_id, event: :answer_ready,
|
|
59
|
+
on_delivery_error: ->(error) { errors << error })
|
|
60
|
+
state
|
|
61
|
+
}
|
|
62
|
+
transition from: :asking, on: :answer_ready, to: :done,
|
|
63
|
+
action: ->(state, event) {
|
|
64
|
+
error = event.payload[:error]
|
|
65
|
+
error ? state.merge(error_message: error.message) : state.merge(answer: event.payload.fetch(:answer))
|
|
66
|
+
}
|
|
67
|
+
transition from: :done, to: :__finish__
|
|
68
|
+
end
|
|
69
|
+
final = workflow.invoke({question: "Explain completion handles"},
|
|
70
|
+
config: {workflow_instance_id: "bounded-recipe"})
|
|
71
|
+
puts final.error_message || final.answer
|
|
72
|
+
raise errors.pop unless errors.empty?
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`forward_completion` returns nil after registering one callback. Already-completed
|
|
76
|
+
results are notified immediately. Two registrations are two subscriptions; the
|
|
77
|
+
application avoids duplicates. No task state is changed, no retry is attempted,
|
|
78
|
+
and no callback waits for execution, storage or network delivery.
|
|
79
|
+
|
|
80
|
+
Operation failures and cancellation notify the Workflow with `{error: error}`.
|
|
81
|
+
A false signal result and a raised signal exception each notify the required
|
|
82
|
+
`on_delivery_error` callback once. A failure of that callback follows TaskResult's
|
|
83
|
+
existing logger policy; it is not retried and does not rewrite the task result.
|
|
84
|
+
The application observes delivery errors and chooses cancellation/recovery.
|
|
85
|
+
|
|
86
|
+
Signal acceptance does not guarantee transition completion or a durable save.
|
|
87
|
+
Incomplete tasks retain their subscriptions until settlement under the existing
|
|
88
|
+
TaskResult lifecycle. The caller still owns operation cancellation and lifetime;
|
|
89
|
+
this recipe installs no Runtime shutdown hook or global subscription registry.
|
|
90
|
+
|
|
91
|
+
See [the full application recipe](application-recipes.md#workflow-completion-and-failures)
|
|
92
|
+
for the same failure behavior without extracting this application-local function.
|