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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ee0a6e2cc02a9014a1f6b4b0587474255697960b0e1589070a7846d038452ad
|
|
4
|
+
data.tar.gz: eb866c788b4dbaf5c560ec0a6a9fe1ef641d630932c7f4309c5ca0741dce2ad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4970e936adca3d87ee8a8a18469630a563df8fb8a07f06796f4a4bfcae3b1e97b67a51869fdbb68760892201deb12f5f765c082cdfe8559c5f6d0c5ad10dd1a
|
|
7
|
+
data.tar.gz: 10c461ef9e86a476cc73020dc1167d6b80564f02f47c3da0161a7da8a34953be96efa236ebcfdf938c7b8a5bfeb2ea6ad3fdaca137d6f70cd5238a71bd6cb829
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ tracking `main` directly.
|
|
|
21
21
|
|
|
22
22
|
- **Agent** — stateful, persistence-backed LLM agent with canonical execution history.
|
|
23
23
|
- **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.
|
|
24
|
+
- **Workflow** — state-machine-driven application workflow with task states, explicit events and wait states.
|
|
25
25
|
- **Tool / Capability** — callable application capability exposed to an Agent; application-defined Tools subclass `Phronomy::Tool::Base`.
|
|
26
26
|
- **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
27
|
- **EventLoop + FSMSession** — the framework control plane for logical lifecycle coordination.
|
|
@@ -35,6 +35,19 @@ tracking `main` directly.
|
|
|
35
35
|
|
|
36
36
|
See [Features and Application Programming Interface (API) stability](docs/features.md) for the full feature matrix.
|
|
37
37
|
|
|
38
|
+
The following application APIs are part of unreleased development:
|
|
39
|
+
|
|
40
|
+
| API | Stability | Contract |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `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) |
|
|
43
|
+
| `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) |
|
|
44
|
+
| `Phronomy::Integrations::OrderedEventDelivery` | Beta | Optional ordered delivery with bounded admission and explicit drain lifetime; [usage](docs/ordered-event-delivery.md) |
|
|
45
|
+
| `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) |
|
|
46
|
+
| `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) |
|
|
47
|
+
| `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) |
|
|
48
|
+
| `Phronomy::Embeddings::AuthenticationError` and `RateLimitError` | Beta | Embeddings-owned transport error subclasses retaining the provider cause; [migration](docs/architecture/error-api-review.md) |
|
|
49
|
+
| `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) |
|
|
50
|
+
|
|
38
51
|
## Installation
|
|
39
52
|
|
|
40
53
|
Add Phronomy to your Gemfile:
|
|
@@ -49,7 +62,10 @@ Then run:
|
|
|
49
62
|
bundle install
|
|
50
63
|
```
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
Phronomy 0.28 requires RubyLLM 2.0.x. Faraday 2.1.0 is the tested minimum
|
|
66
|
+
supported version; the `~> 2.1` requirement allows versions from 2.1 up to,
|
|
67
|
+
but excluding, 3.0. CI checks fresh dependency resolution and the exact minimum
|
|
68
|
+
with MCP 1.0.0. See the
|
|
53
69
|
[RubyLLM 2 and token-ownership migration](docs/architecture/rubyllm-2-token-ownership.md)
|
|
54
70
|
for removed input-budget overrides and the in-flight Tool-manifest upgrade boundary.
|
|
55
71
|
|
|
@@ -91,9 +107,7 @@ result = ResearchAgent.new.invoke("What happened in AI research this week?")
|
|
|
91
107
|
puts result[:output]
|
|
92
108
|
```
|
|
93
109
|
|
|
94
|
-
`Phronomy::Tool::Base` is the public
|
|
95
|
-
class. The legacy `Phronomy::Tool::Base` constant remains
|
|
96
|
-
valid for compatibility.
|
|
110
|
+
`Phronomy::Tool::Base` is the public base class for application-defined Tools.
|
|
97
111
|
|
|
98
112
|
For non-blocking top-level use, call `invoke_async` and keep the returned
|
|
99
113
|
`Phronomy::TaskResult`. Inside Phronomy lifecycle callbacks, do not block waiting for
|
|
@@ -152,6 +166,7 @@ contracts, timeout/cancellation semantics, metrics, and callback rules.
|
|
|
152
166
|
## Documentation
|
|
153
167
|
|
|
154
168
|
- [Getting started](docs/getting-started.md) — installation, RubyLLM setup, Agent/Workflow basics, persistence, streaming.
|
|
169
|
+
- [Application recipes](docs/application-recipes.md) — complete persistence, streaming, and Workflow examples, including explicit failure handling.
|
|
155
170
|
- [Features and API stability](docs/features.md) — public feature matrix and stability labels.
|
|
156
171
|
- [Architecture](docs/architecture.md) — canonical current explanatory architecture entry and authority navigation.
|
|
157
172
|
- [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.
|
|
@@ -0,0 +1,119 @@
|
|
|
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 the main operation failed, its same exception is preserved and secondary
|
|
87
|
+
cleanup failures are logged with Agent IDs. Logger failure cannot replace it.
|
|
88
|
+
If the main operation succeeded, failed cleanup raises `Agent::ScopeCleanupError`
|
|
89
|
+
(`code: "agent.scope_cleanup_failed"`). `result` preserves the successful value;
|
|
90
|
+
`failures` is an array of `{agent_id:, error:}` with the original exceptions.
|
|
91
|
+
All acquired Agents are attempted. No automatic retry, purge, or retention
|
|
92
|
+
removal is performed.
|
|
93
|
+
|
|
94
|
+
## Responsibility and ordering
|
|
95
|
+
|
|
96
|
+
- C04 owns scope receipts, exact incarnation identity, operation admission,
|
|
97
|
+
busy/retention rules, invalidation and cleanup errors.
|
|
98
|
+
- C07 supplies the existing transaction boundary. C04's Agent Store supplies
|
|
99
|
+
`assert_idle!`, `guard_agent!` and retention reads; neither Persistence nor
|
|
100
|
+
physical Storage gains incarnation release policy.
|
|
101
|
+
- C13 supplies existing TaskResult and physical-completion contracts. It gains
|
|
102
|
+
no scope membership, Agent busy rules or unload states.
|
|
103
|
+
- Engine supplies existing worker execution, serial EventLoop delivery and
|
|
104
|
+
shutdown participation. It gains no Agent-specific states.
|
|
105
|
+
- The operation lease and release reservation share the C04 ownership mutex.
|
|
106
|
+
No Store transaction, Runtime call, application callback or blocking wait runs
|
|
107
|
+
under that mutex. Accepted runner continuations use C04 internal snapshots;
|
|
108
|
+
callbacks initiating a new public operation must acquire a new lease.
|
|
109
|
+
- Agent operation completion is distinct from cancelling its public handle.
|
|
110
|
+
C04 acknowledges release after domain terminal/listener delivery and physical
|
|
111
|
+
worker completion. A queued or physically running cancelled operation still
|
|
112
|
+
blocks release.
|
|
113
|
+
|
|
114
|
+
For notifications, finish child processing, drain notifications, then exit the
|
|
115
|
+
Agent scope. `run_once` retains its existing Store and lifetime behavior; merely
|
|
116
|
+
calling it does not opt into scope cleanup.
|
|
117
|
+
|
|
118
|
+
This is P5 (core). P6 migrates application examples and performs cross-repository
|
|
119
|
+
acceptance against the published or pinned candidate core.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# DX05-D: Workflow-owned asynchronous task states
|
|
2
|
+
|
|
3
|
+
Status: unreleased Beta implementation. Based on DX05-A core `c673c958c`.
|
|
4
|
+
|
|
5
|
+
## Problem and intended application boundary
|
|
6
|
+
|
|
7
|
+
A Workflow application describes which operation a stage starts, how its result
|
|
8
|
+
changes business state, and where that state goes next. Repeating callbacks,
|
|
9
|
+
completion events, error envelopes and state-entry identifiers obscures those
|
|
10
|
+
choices. Workflow already owns the live execution and state-entry lifetime, so it
|
|
11
|
+
can own this correlation without knowing how an Agent, Tool or LLM runs.
|
|
12
|
+
|
|
13
|
+
The business flow is preserved. Failure, cancellation, stale-result and delivery
|
|
14
|
+
semantics become explicit and stronger; this is not a claim that every failure
|
|
15
|
+
path of the previous manual recipe was equivalent.
|
|
16
|
+
|
|
17
|
+
## Before and after
|
|
18
|
+
|
|
19
|
+
Previously, each state manually arranged delivery (simplified successful path):
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
workflow = nil
|
|
23
|
+
workflow = Phronomy::Workflow.define(AnswerState) do
|
|
24
|
+
initial :asking
|
|
25
|
+
state :asking
|
|
26
|
+
entry :asking, ->(state) {
|
|
27
|
+
id = state.workflow_instance_id
|
|
28
|
+
agent.invoke_async(state.question).on_complete do |value, error|
|
|
29
|
+
workflow.signal(workflow_instance_id: id, event: :answer_ready,
|
|
30
|
+
payload: {value: value, error: error})
|
|
31
|
+
end
|
|
32
|
+
state
|
|
33
|
+
}
|
|
34
|
+
transition from: :asking, on: :answer_ready, to: :__finish__,
|
|
35
|
+
action: ->(state, event) {
|
|
36
|
+
raise event.payload[:error] if event.payload[:error]
|
|
37
|
+
state.merge(answer: event.payload.fetch(:value).fetch(:output))
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That snippet also needs rejection handling and entry-specific guards for safe
|
|
43
|
+
reentry. The application now writes only the operation and successful update:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
workflow = Phronomy::Workflow.define(AnswerState) do
|
|
47
|
+
initial :asking
|
|
48
|
+
task_state :asking,
|
|
49
|
+
start: ->(state) { agent.invoke_async(state.question) },
|
|
50
|
+
apply: ->(state, result) { state.merge(answer: result.fetch(:output)) }
|
|
51
|
+
transition from: :asking, to: :__finish__
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`start` receives an isolated context snapshot, including current phase and
|
|
56
|
+
Workflow identity. It must return a `Phronomy::TaskResult` promptly. It must not
|
|
57
|
+
perform blocking I/O or wait for another Task. Use `Blocking.call_async` for real
|
|
58
|
+
blocking work. `apply` receives the current context and successful value, runs
|
|
59
|
+
briefly on EventLoop, and returns the same context type or nil. Normal transition
|
|
60
|
+
guards run after `apply`. Nil means no context replacement, not failure.
|
|
61
|
+
|
|
62
|
+
A task state may have ordinary entry/exit actions and external transitions.
|
|
63
|
+
Leaving it abandons observation of its source. Declaring the same state as both
|
|
64
|
+
`state`/`wait_state` and `task_state` is rejected. Ordinary entry/transition actions
|
|
65
|
+
still cannot return a TaskResult. Business signals keep their existing API.
|
|
66
|
+
|
|
67
|
+
## Contract ownership
|
|
68
|
+
|
|
69
|
+
| Owner | Responsibility |
|
|
70
|
+
|---|---|
|
|
71
|
+
| C02 Workflow | Task-state definition; live execution and state-entry reception; once-only consumption; success application, failure/cancellation classification; stale-result and queued-progression rejection; restart policy |
|
|
72
|
+
| C13 Execution | Thread-safe TaskResult notification and cancellation subscription mechanisms |
|
|
73
|
+
| I07 Workflow runtime binding | Connect C02 decisions to the existing FSM, owner EventLoop and owner Runtime timer/shutdown services |
|
|
74
|
+
| Engine | Generic event/session delivery and lifecycle; no task-state or Agent policy is added |
|
|
75
|
+
| C04 Agent and application | Agent execution, business transformation/fallback, child cancellation policy and producer lifetime |
|
|
76
|
+
|
|
77
|
+
C02 uses its execution-environment port and receives an injected session
|
|
78
|
+
connection. It does not select Runtime or depend on I07. `WorkflowTaskSession`
|
|
79
|
+
in I07 adapts the generic FSMSession. Engine source is unchanged.
|
|
80
|
+
|
|
81
|
+
A revocable C02 reception is created before `start`. Source callbacks retain only
|
|
82
|
+
that reception. Closing it drops source and registry references. Completion is
|
|
83
|
+
sent to the originating registry under an opaque execution owner token, never
|
|
84
|
+
rerouted by a durable Workflow ID. Receiver-side validation checks the current
|
|
85
|
+
entry and consumes once. Exits, reentry, cancellation and terminal decisions
|
|
86
|
+
revoke it. Queued automatic progression also carries an entry-specific key:
|
|
87
|
+
an external transition between result application and automatic dispatch cannot
|
|
88
|
+
advance a later entry. These private messages are not public business signals.
|
|
89
|
+
|
|
90
|
+
## Outcomes and lifetime
|
|
91
|
+
|
|
92
|
+
| Condition | Caller-visible result |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Start throws, source fails, or apply throws | Workflow fails with the same error object |
|
|
95
|
+
| Source status is `cancelled` | Workflow TaskResult is cancelled with the source error |
|
|
96
|
+
| Source status is `failed` with a CancellationError | Remains failed; class alone does not imply cancellation |
|
|
97
|
+
| Explicit parent token cancellation/deadline | Cancel Workflow observation; do not implicitly cancel the source |
|
|
98
|
+
| Old or duplicated source notification | Ignore; no application or retry |
|
|
99
|
+
| Current completion delivery rejects/throws | Fail on the owner; if owner delivery is wholly unavailable, settle observation only and retain admission until shutdown cleanup |
|
|
100
|
+
| Runtime shutdown while waiting | Settle observation and revoke subscriptions; no claim that child work physically stopped |
|
|
101
|
+
| Known durable terminal result | Use existing save/readback classification and release rules |
|
|
102
|
+
| Unknown durable terminal result | Fail the observation while retaining recovery-required ownership |
|
|
103
|
+
|
|
104
|
+
Cancellation is serialized on the owning EventLoop. Once terminal persistence
|
|
105
|
+
has begun, its existing durable barrier remains authoritative; later cancellation
|
|
106
|
+
does not reverse a committed or uncertain terminal decision. A source passed to
|
|
107
|
+
the state has an independent lifetime. Applications explicitly pass cancellation
|
|
108
|
+
controls to children when desired; they own Agent release and side effects.
|
|
109
|
+
|
|
110
|
+
This API creates no thread/worker for logical waits. Actual I/O and existing
|
|
111
|
+
tracing or checkpoint work may still use the bounded pool. Expected business
|
|
112
|
+
failures may be transformed into ordinary values through TaskResult composition.
|
|
113
|
+
The API does not infer retry, fallback, durable child execution or delivery ACKs.
|
|
114
|
+
|
|
115
|
+
## Persistence and restart
|
|
116
|
+
|
|
117
|
+
Normal completion and ordinary `wait_state` halts use existing Workflow
|
|
118
|
+
checkpoints. Resuming an ordinary wait into a new task state is supported. Pending
|
|
119
|
+
TaskResults, callbacks and entry keys are process-local and are never serialized.
|
|
120
|
+
Restoring a snapshot whose phase is a task state raises
|
|
121
|
+
`Phronomy::WorkflowTaskResumeError` (C02, subclass of `Phronomy::Error`, code
|
|
122
|
+
`workflow.task_resume_unsupported`) before running its start callback. The
|
|
123
|
+
application must reconcile that operation explicitly instead of automatically
|
|
124
|
+
repeating external side effects. F01 durable child execution remains separate.
|
|
125
|
+
|
|
126
|
+
## Verification
|
|
127
|
+
|
|
128
|
+
The new task-state regression suite covers immediate and delayed results,
|
|
129
|
+
repeated entry, old success/failure/cancellation, duplicate callbacks, external
|
|
130
|
+
transition races, rejected guards, delivery failure, shutdown, source/parent
|
|
131
|
+
cancellation, owner Runtime selection, retained-reference revocation, strict
|
|
132
|
+
callback result types, normal durable halt/resume, restart rejection and unknown
|
|
133
|
+
durable outcomes. Existing Workflow, TaskResult, API and executable-documentation
|
|
134
|
+
tests remain active. Examples 01/03/04/07/14/20/25/26 use the new API.
|