phronomy 0.20.0 → 0.22.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/.mutant.yml +3 -1
- data/CHANGELOG.md +41 -11
- data/CONTRIBUTING.md +44 -6
- data/README.md +26 -6
- data/docs/decisions/010-cooperative-first-concurrency.md +86 -69
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +184 -0
- data/docs/features.md +22 -13
- data/docs/getting-started.md +36 -18
- data/docs/runtime-and-concurrency.md +110 -147
- data/examples/workflows/agent_event_mapping.rb +27 -30
- data/lib/phronomy/agent/async_event_api.rb +14 -4
- data/lib/phronomy/agent/tool_executor.rb +7 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +142 -245
- data/lib/phronomy/engine/task.rb +50 -6
- data/lib/phronomy/invocation_context.rb +11 -52
- data/lib/phronomy/llm_adapter/base.rb +29 -32
- data/lib/phronomy/llm_adapter/ruby_llm.rb +13 -12
- data/lib/phronomy/llm_adapter.rb +10 -7
- data/lib/phronomy/output_parser/base.rb +5 -1
- data/lib/phronomy/tool/base.rb +15 -0
- data/lib/phronomy/tool.rb +11 -0
- data/lib/phronomy/vector_store/async_backend.rb +15 -38
- data/lib/phronomy/vector_store/base.rb +12 -13
- data/lib/phronomy/vector_store/embeddings/base.rb +11 -9
- data/lib/phronomy/version.rb +1 -1
- data/scripts/run_mutation.sh +2 -1
- data/sig/phronomy/agent.rbs +36 -0
- data/sig/phronomy/extensions.rbs +50 -0
- data/sig/phronomy/llm_adapter.rbs +11 -0
- data/sig/phronomy/persistence.rbs +70 -0
- data/sig/phronomy/runtime.rbs +43 -0
- data/sig/phronomy/tool.rbs +39 -0
- data/sig/phronomy/workflow.rbs +30 -0
- data/sig/phronomy.rbs +49 -1
- metadata +12 -2
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# Runtime and concurrency
|
|
2
2
|
|
|
3
3
|
Phronomy uses an **EventLoop / FSMSession first** architecture for framework
|
|
4
|
-
lifecycle coordination. `FSMSession`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`OffloadPool`.
|
|
4
|
+
lifecycle coordination. `FSMSession` represents explicit lifecycle state and
|
|
5
|
+
events. `Phronomy::Task` is the common caller-facing completion handle, not an
|
|
6
|
+
execution backend. Synchronous work that must stay off EventLoop is isolated in
|
|
7
|
+
the bounded `OffloadPool`.
|
|
8
8
|
|
|
9
|
-
For the design rationale, see
|
|
10
|
-
[ADR-010: EventLoop / FSMSession First Concurrency](decisions/010-cooperative-first-concurrency.md).
|
|
9
|
+
For the design rationale, see [ADR-010](decisions/010-cooperative-first-concurrency.md).
|
|
11
10
|
Durable-state ownership is defined by
|
|
12
|
-
[ADR-014
|
|
11
|
+
[ADR-014](decisions/014-unified-persistence-durable-state.md).
|
|
13
12
|
|
|
14
13
|
## Runtime model
|
|
15
14
|
|
|
@@ -23,17 +22,21 @@ Runtime
|
|
|
23
22
|
│ └─ MultiAgent fan-out
|
|
24
23
|
├─ process-local Agent ActivationRegistry
|
|
25
24
|
├─ OffloadPool (bounded operating-system Threads)
|
|
25
|
+
│ ├─ private Operation records
|
|
26
26
|
│ ├─ blocking input/output (I/O)
|
|
27
27
|
│ ├─ central-processing-unit (CPU)-bound synchronous work
|
|
28
28
|
│ └─ other long synchronous work
|
|
29
29
|
├─ named OffloadPools
|
|
30
30
|
└─ EventLoop-driven timers
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
EventLoop / FSMSession ─┐
|
|
33
|
+
├─> Task = completion handle
|
|
34
|
+
OffloadPool ────────────┘
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
The framework does not allocate one operating-system Thread per logical
|
|
36
|
-
lifecycle. Logical waits remain explicit states plus later
|
|
37
|
+
The framework does not allocate one operating-system Thread per logical
|
|
38
|
+
Agent/Workflow/Tool lifecycle. Logical waits remain explicit states plus later
|
|
39
|
+
EventLoop events.
|
|
37
40
|
|
|
38
41
|
## Live state and durable state
|
|
39
42
|
|
|
@@ -49,8 +52,8 @@ external writer that advances the durable base causes `Persistence::ConflictErro
|
|
|
49
52
|
rather than automatic reload or merge.
|
|
50
53
|
|
|
51
54
|
For Workflows, the current `WorkflowContext` and FSMSession own the active
|
|
52
|
-
logical state. A durable Workflow hydrates once at invocation/resume and saves
|
|
53
|
-
|
|
55
|
+
logical state. A durable Workflow hydrates once at invocation/resume and saves at
|
|
56
|
+
the halted/terminal boundary.
|
|
54
57
|
|
|
55
58
|
Content-addressed `Persistence#contents` values are immutable. Fetching a known
|
|
56
59
|
content reference is value materialization rather than mutable state refresh.
|
|
@@ -61,56 +64,44 @@ Workflow execution keeps three identities separate:
|
|
|
61
64
|
|
|
62
65
|
```text
|
|
63
66
|
session_id
|
|
64
|
-
application session/correlation identity
|
|
67
|
+
application session/correlation identity
|
|
65
68
|
|
|
66
69
|
thread_id
|
|
67
70
|
durable Workflow identity and Persistence#workflow_states key
|
|
68
71
|
|
|
69
72
|
fsm_session_id
|
|
70
|
-
one Runtime FSMSession execution identity; generated again for each
|
|
71
|
-
invoke/resume operation
|
|
73
|
+
one Runtime FSMSession execution identity; generated again for each invoke/resume
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
The
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
The application `session_id` is tracing/caller metadata and is not used for
|
|
77
|
+
durable Workflow ownership. EventLoop registers active FSMs by `fsm_session_id`;
|
|
78
|
+
durable Workflow admission is a separate owner map:
|
|
77
79
|
|
|
78
80
|
```text
|
|
79
81
|
thread_id -> owner_fsm_session_id
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
The owner is acquired before `workflow_states.load(thread_id)` and remains held
|
|
83
|
-
until the halted/terminal `workflow_states.save(...)` completes.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
The admission map belongs to one Runtime and is process-local. It prevents two
|
|
88
|
-
executions with the same durable `thread_id` from being admitted concurrently
|
|
89
|
-
inside that Runtime, but it is not shared across Ruby processes, containers, or
|
|
90
|
-
service replicas. Separate processes may therefore execute the same `thread_id`
|
|
91
|
-
concurrently unless the application adds distributed coordination.
|
|
92
|
-
|
|
93
|
-
`workflow_states` optimistic revisions detect stale terminal commits across those
|
|
94
|
-
processes. They do not prevent duplicate execution from starting and cannot undo
|
|
95
|
-
external side effects that both executions already performed before one save
|
|
96
|
-
loses the revision race. CAS is stale/double-commit detection, not a distributed
|
|
97
|
-
execution lock or duplicate-side-effect prevention mechanism.
|
|
85
|
+
until the halted/terminal `workflow_states.save(...)` completes. The admission map
|
|
86
|
+
is process-local. Cross-process duplicate execution requires application-level
|
|
87
|
+
distributed coordination; optimistic revisions detect stale terminal commits but
|
|
88
|
+
do not prevent duplicate side effects before that conflict is detected.
|
|
98
89
|
|
|
99
90
|
## Tool execution modes
|
|
100
91
|
|
|
101
92
|
Phronomy exposes two execution modes for capabilities:
|
|
102
93
|
|
|
103
|
-
- `:cooperative` — short EventLoop-safe work, or
|
|
104
|
-
that starts another Phronomy lifecycle and returns immediately
|
|
94
|
+
- `:cooperative` — short EventLoop-safe work, or specialized asynchronous work
|
|
95
|
+
that starts another Phronomy lifecycle and returns a Task immediately;
|
|
105
96
|
- `:offloaded` — synchronous work that must not run to completion on EventLoop.
|
|
106
97
|
|
|
98
|
+
Both paths return `Phronomy::Task`. The execution mechanism differs; the
|
|
99
|
+
completion abstraction does not.
|
|
100
|
+
|
|
107
101
|
Phronomy does not classify application work into framework-level I/O/CPU/process
|
|
108
|
-
execution modes.
|
|
102
|
+
execution modes. Workload classification and capacity planning belong to the
|
|
109
103
|
application.
|
|
110
104
|
|
|
111
|
-
A CPU-heavy operation may therefore use `:offloaded`, but thread offload does not
|
|
112
|
-
remove CRuby Global VM Lock contention or physical CPU contention.
|
|
113
|
-
|
|
114
105
|
## Logical waiting versus offload
|
|
115
106
|
|
|
116
107
|
Do not offload a logical wait merely to make it asynchronous.
|
|
@@ -129,7 +120,7 @@ Correct shape:
|
|
|
129
120
|
parent FSMSession
|
|
130
121
|
→ start child lifecycle
|
|
131
122
|
→ return immediately
|
|
132
|
-
→ child settles
|
|
123
|
+
→ child Task settles
|
|
133
124
|
→ post parent EventLoop event
|
|
134
125
|
```
|
|
135
126
|
|
|
@@ -141,22 +132,13 @@ waiting at the same time.
|
|
|
141
132
|
`Persistence` repositories expose synchronous operations. Framework lifecycle
|
|
142
133
|
code must not perform potentially blocking durable reads/writes on EventLoop.
|
|
143
134
|
Agent preparation/commit and Workflow hydrate/save operations are submitted to
|
|
144
|
-
`OffloadPool`; completion continues through
|
|
145
|
-
|
|
135
|
+
`OffloadPool`; completion continues through Task callbacks or explicit EventLoop
|
|
136
|
+
events.
|
|
146
137
|
|
|
147
138
|
A durable barrier may pause one logical lifecycle without blocking EventLoop.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Approval wait is not a hydration boundary. The same live Agent instance,
|
|
153
|
-
Activation, and AgentInvocation remain the owner and are resumed after approval.
|
|
154
|
-
Approval itself remains an Agent-instance operation. An application that only has
|
|
155
|
-
an `execution_id` first resolves the current process's owner with
|
|
156
|
-
`Phronomy::Agent::Base.live_for_execution(execution_id)` or the expected concrete
|
|
157
|
-
Agent class, then calls `agent.approve(...)` or `agent.approve_async(...)`.
|
|
158
|
-
`live_for_execution` consults the Runtime-local ActivationRegistry and does not
|
|
159
|
-
load a replacement Agent from Persistence.
|
|
139
|
+
Persistence does not implement async repository variants and must not depend on
|
|
140
|
+
EventLoop, FSMSession, Task settlement internals, or private OffloadPool operation
|
|
141
|
+
records.
|
|
160
142
|
|
|
161
143
|
## Sync versus async application APIs
|
|
162
144
|
|
|
@@ -172,23 +154,37 @@ load a replacement Agent from Persistence.
|
|
|
172
154
|
|
|
173
155
|
Blocking synchronous APIs reject EventLoop re-entry with
|
|
174
156
|
`Phronomy::EventLoopReentrancyError` when waiting would stall the same EventLoop
|
|
175
|
-
needed for progress.
|
|
176
|
-
registry lookup and does not wait for Task progress.
|
|
157
|
+
needed for progress.
|
|
177
158
|
|
|
178
159
|
## Task
|
|
179
160
|
|
|
180
161
|
`Phronomy::Task` is thread-free. It represents one terminal result:
|
|
181
162
|
|
|
182
|
-
- completed value
|
|
183
|
-
- failure
|
|
163
|
+
- completed value;
|
|
164
|
+
- failure;
|
|
184
165
|
- cancellation.
|
|
185
166
|
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
Task is the common completion abstraction for logical EventLoop/FSMSession
|
|
168
|
+
lifecycles and OffloadPool-backed synchronous work.
|
|
169
|
+
|
|
170
|
+
`Task#wait_result(timeout:)` is a bridge for external synchronous callers. Its
|
|
171
|
+
timeout is waiter-local: it does not settle/cancel the Task or alter OffloadPool
|
|
172
|
+
abandonment state.
|
|
173
|
+
|
|
174
|
+
`Task#on_complete` registers an independent notification callback. Callback
|
|
175
|
+
execution thread is not guaranteed. A callback may be delivered by an OffloadPool
|
|
176
|
+
worker, a timer/cancellation caller, an EventLoop-related control path, or the
|
|
177
|
+
thread that registers after settlement. Callbacks must be thread-safe and should
|
|
178
|
+
complete quickly.
|
|
188
179
|
|
|
189
180
|
`Task#map` is application-level composition. A transformation exception settles
|
|
190
|
-
the mapped Task as failed.
|
|
191
|
-
|
|
181
|
+
the mapped Task as failed.
|
|
182
|
+
|
|
183
|
+
Framework components own Task settlement. Application code should not use
|
|
184
|
+
`Task#complete`, `Task#fail`, or `Task#cancel!` as operation-control APIs. Request
|
|
185
|
+
operation-wide cancellation through the `CancellationToken` accepted by the API
|
|
186
|
+
that created the Task. Task settlement never propagates backwards to cancel a
|
|
187
|
+
shared CancellationToken.
|
|
192
188
|
|
|
193
189
|
## OffloadPool
|
|
194
190
|
|
|
@@ -197,131 +193,98 @@ on EventLoop.
|
|
|
197
193
|
|
|
198
194
|
Its guarantees include:
|
|
199
195
|
|
|
200
|
-
- bounded worker count
|
|
201
|
-
- bounded queue depth
|
|
202
|
-
- queue backpressure
|
|
203
|
-
- operation-wide submit timeout/cancellation settlement
|
|
204
|
-
- abandoned-worker accounting
|
|
205
|
-
- runtime metrics
|
|
196
|
+
- bounded worker count;
|
|
197
|
+
- bounded queue depth;
|
|
198
|
+
- queue backpressure;
|
|
199
|
+
- operation-wide submit timeout/cancellation settlement;
|
|
200
|
+
- abandoned-worker accounting;
|
|
201
|
+
- runtime metrics;
|
|
206
202
|
- shutdown/drain behavior.
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
204
|
+
`OffloadPool#submit` returns a `Phronomy::Task`. OffloadPool does not expose its
|
|
205
|
+
execution record as a caller-facing future/promise. Its private `Operation` owns:
|
|
206
|
+
|
|
207
|
+
- the submitted block;
|
|
208
|
+
- queue submission/start timing;
|
|
209
|
+
- worker-start linearization;
|
|
210
|
+
- submit timeout/cancellation flags;
|
|
211
|
+
- abandoned state;
|
|
212
|
+
- cancellation callback lifecycle;
|
|
213
|
+
- metrics state needed by the pool.
|
|
214
|
+
|
|
215
|
+
This separation keeps execution details private while allowing every asynchronous
|
|
216
|
+
Phronomy API to expose the same Task completion contract.
|
|
210
217
|
|
|
211
218
|
### EventLoop queue admission
|
|
212
219
|
|
|
213
220
|
Framework-owned EventLoop-origin submissions must not wait for a free worker
|
|
214
221
|
queue slot. They use non-blocking admission (`on_full: :raise`) and route
|
|
215
|
-
`BackpressureError` through the ordinary FSM/completion path.
|
|
222
|
+
`BackpressureError` through the ordinary FSM/Task completion path.
|
|
216
223
|
|
|
217
224
|
External management threads may choose a blocking admission policy when blocking
|
|
218
225
|
the caller is acceptable.
|
|
219
226
|
|
|
220
|
-
## PendingOperation and blocking_wait
|
|
221
|
-
|
|
222
|
-
`OffloadPool#submit` returns a private `PendingOperation` immediately after queue
|
|
223
|
-
admission.
|
|
224
|
-
|
|
225
|
-
`PendingOperation#blocking_wait(timeout:)` is intentionally a **low-level
|
|
226
|
-
synchronous bridge** for non-EventLoop callers such as tests and diagnostics.
|
|
227
|
-
The timeout belongs only to that waiter:
|
|
228
|
-
|
|
229
|
-
- it raises `TimeoutError` to that calling thread,
|
|
230
|
-
- it does not settle the PendingOperation,
|
|
231
|
-
- it does not cancel the submitted operation,
|
|
232
|
-
- it does not mark the operation abandoned.
|
|
233
|
-
|
|
234
|
-
There is no waiter-local `cancellation_token:` argument. Operation-wide
|
|
235
|
-
cancellation belongs exclusively to `OffloadPool#submit(cancellation_token:)`.
|
|
236
|
-
|
|
237
227
|
## Submit timeout and cancellation
|
|
238
228
|
|
|
239
|
-
Submit-time timeout and submit cancellation settle the caller-facing
|
|
240
|
-
|
|
229
|
+
Submit-time timeout and submit cancellation settle the caller-facing Task. They
|
|
230
|
+
do **not** asynchronously interrupt an already-running synchronous worker.
|
|
241
231
|
|
|
242
232
|
### Before worker start
|
|
243
233
|
|
|
244
234
|
If timeout/cancellation wins before execution starts:
|
|
245
235
|
|
|
246
|
-
- the
|
|
247
|
-
- the submitted block does not run
|
|
248
|
-
- the
|
|
236
|
+
- the Task settles (`TimeoutError` failure or cancellation);
|
|
237
|
+
- the submitted block does not run;
|
|
238
|
+
- the private Operation is not counted as abandoned.
|
|
249
239
|
|
|
250
240
|
### After worker start
|
|
251
241
|
|
|
252
242
|
If timeout/cancellation wins after execution starts:
|
|
253
243
|
|
|
254
|
-
- the
|
|
255
|
-
- the
|
|
256
|
-
- the worker
|
|
244
|
+
- the Task settles immediately;
|
|
245
|
+
- the private Operation is marked abandoned;
|
|
246
|
+
- the worker continues until its synchronous call returns;
|
|
257
247
|
- the eventual worker result is discarded.
|
|
258
248
|
|
|
259
249
|
Phronomy does not use `Thread#raise` to inject an exception into the worker.
|
|
260
250
|
Application/library code that needs hard or transport-level deadlines should use
|
|
261
|
-
its native timeout or
|
|
251
|
+
its native timeout or an appropriate future process-isolation mechanism.
|
|
262
252
|
|
|
263
253
|
### CancellationToken deadlines
|
|
264
254
|
|
|
265
|
-
`CancellationToken.timeout_after(seconds)` uses a monotonic deadline.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
Components requiring callback delivery for a monotonic deadline must promote the
|
|
270
|
-
deadline to explicit `cancel!` through the Runtime timer queue. OffloadPool does
|
|
271
|
-
this for its submit cancellation token.
|
|
272
|
-
|
|
273
|
-
`CancellationScope#deadline_in` is appropriate when the application needs a
|
|
274
|
-
Runtime-timer-backed cancellation scope whose `on_cancel` subscribers are fired
|
|
275
|
-
on expiry.
|
|
276
|
-
|
|
277
|
-
## Independent notification callbacks
|
|
278
|
-
|
|
279
|
-
Independent notification fan-out is fault-isolated.
|
|
255
|
+
`CancellationToken.timeout_after(seconds)` uses a monotonic deadline. Components
|
|
256
|
+
requiring callback delivery promote the deadline to explicit `cancel!` through
|
|
257
|
+
the Runtime timer queue. OffloadPool does this for its submit cancellation token.
|
|
280
258
|
|
|
281
|
-
|
|
259
|
+
A CancellationToken may be shared by multiple operations. For that reason,
|
|
260
|
+
settling or cancelling one Task does not cancel the token in the reverse
|
|
261
|
+
direction.
|
|
282
262
|
|
|
283
|
-
|
|
284
|
-
- `Task#on_complete`,
|
|
285
|
-
- `PendingOperation#on_complete`,
|
|
286
|
-
- EventLoop timer callbacks.
|
|
263
|
+
## Native async boundary
|
|
287
264
|
|
|
288
|
-
A
|
|
289
|
-
|
|
265
|
+
A genuine native-async driver that does not create a Phronomy-owned OS Thread and
|
|
266
|
+
does not block EventLoop need not consume an OffloadPool worker. If Phronomy
|
|
267
|
+
formally exposes such an extension point, it must adapt completion to
|
|
268
|
+
`Phronomy::Task` rather than exposing a provider-specific future or a private
|
|
269
|
+
Runtime type.
|
|
290
270
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
Callbacks must therefore be thread-safe and should complete quickly. Framework
|
|
297
|
-
lifecycle code normally turns completion into an explicit EventLoop event rather
|
|
298
|
-
than mutating unrelated logical state from a worker thread.
|
|
271
|
+
The current Persistence, VectorStore, Embeddings, and LLM call-extension
|
|
272
|
+
contracts are synchronous at the external implementation boundary where
|
|
273
|
+
applicable. Phronomy owns the OffloadPool wrapper for synchronous work. The
|
|
274
|
+
current VectorStore async methods are framework convenience methods, not a
|
|
275
|
+
native-async backend SPI.
|
|
299
276
|
|
|
300
277
|
## Abandoned-worker metrics
|
|
301
278
|
|
|
302
279
|
Two metrics answer different operational questions:
|
|
303
280
|
|
|
304
281
|
- `offload_pool_abandoned_total` — cumulative count of operations that became
|
|
305
|
-
abandoned after worker execution had started
|
|
282
|
+
abandoned after worker execution had started;
|
|
306
283
|
- `offload_pool_abandoned_active` — current number of abandoned operations whose
|
|
307
284
|
synchronous workers still occupy pool capacity.
|
|
308
285
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```text
|
|
312
|
-
offload_pool_size = 10
|
|
313
|
-
offload_pool_active = 10
|
|
314
|
-
offload_pool_abandoned_active = 8
|
|
315
|
-
offload_pool_abandoned_total = 523
|
|
316
|
-
offload_pool_queue_length = 40
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
This means 10 workers are currently executing, 8 of them are doing work whose
|
|
320
|
-
caller-facing result has already been abandoned, 523 abandonment events have
|
|
321
|
-
occurred since process start, and 40 operations are queued.
|
|
322
|
-
|
|
323
|
-
`Phronomy::Diagnostics.dump` exposes the same distinction for point-in-time
|
|
324
|
-
troubleshooting.
|
|
286
|
+
The abandonment state belongs to the private OffloadPool Operation, not to Task.
|
|
287
|
+
Task reports only caller-facing settlement.
|
|
325
288
|
|
|
326
289
|
## EventLoop metrics
|
|
327
290
|
|
|
@@ -344,6 +307,6 @@ Runtime shutdown before resetting configuration.
|
|
|
344
307
|
## Further design records
|
|
345
308
|
|
|
346
309
|
The `docs/decisions/` directory contains the historical and current Architecture
|
|
347
|
-
Decision Records
|
|
348
|
-
section/current ADR as the active design contract and keep
|
|
349
|
-
|
|
310
|
+
Decision Records. When an older ADR is superseded, use the superseding
|
|
311
|
+
section/current ADR as the active design contract and keep earlier documents as
|
|
312
|
+
historical rationale.
|
|
@@ -42,36 +42,33 @@ workflow = Phronomy::Workflow.define(GenerationContext) do
|
|
|
42
42
|
request_id = context.generation_request_id
|
|
43
43
|
|
|
44
44
|
# The Agent Task is intentionally not returned from the entry action.
|
|
45
|
-
#
|
|
46
|
-
agent.invoke_async(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
)
|
|
45
|
+
# The block is the application-level integration channel.
|
|
46
|
+
agent.invoke_async(context.prompt) do |agent_event|
|
|
47
|
+
workflow_event =
|
|
48
|
+
case agent_event.type
|
|
49
|
+
when :done
|
|
50
|
+
:generation_completed
|
|
51
|
+
when :error, :timeout, :cancelled, :approval_required
|
|
52
|
+
:generation_failed
|
|
53
|
+
end
|
|
54
|
+
next unless workflow_event
|
|
55
|
+
|
|
56
|
+
workflow.signal(
|
|
57
|
+
thread_id: context.thread_id,
|
|
58
|
+
event: workflow_event,
|
|
59
|
+
payload: {
|
|
60
|
+
generation_request_id: request_id,
|
|
61
|
+
agent_result: (
|
|
62
|
+
agent_event.payload if agent_event.type == :done
|
|
63
|
+
),
|
|
64
|
+
error:
|
|
65
|
+
agent_event.payload[:error] ||
|
|
66
|
+
Phronomy::Error.new(
|
|
67
|
+
"Agent requested Tool approval"
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
end
|
|
75
72
|
|
|
76
73
|
context
|
|
77
74
|
}
|
|
@@ -5,7 +5,15 @@ module Phronomy
|
|
|
5
5
|
# Public Agent execution API. Agent instances are always stateful and all
|
|
6
6
|
# operations are coordinated through Phronomy::Persistence.
|
|
7
7
|
module AsyncEventApi
|
|
8
|
-
def invoke(
|
|
8
|
+
def invoke(
|
|
9
|
+
input,
|
|
10
|
+
thread_id: nil,
|
|
11
|
+
config: {},
|
|
12
|
+
invocation_context: nil,
|
|
13
|
+
on_event: nil,
|
|
14
|
+
&block
|
|
15
|
+
)
|
|
16
|
+
listener = resolve_event_listener(on_event, block)
|
|
9
17
|
if invocation_context
|
|
10
18
|
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
11
19
|
end
|
|
@@ -15,7 +23,7 @@ module Phronomy
|
|
|
15
23
|
input,
|
|
16
24
|
thread_id: thread_id,
|
|
17
25
|
config: config,
|
|
18
|
-
on_event:
|
|
26
|
+
on_event: listener
|
|
19
27
|
).wait_result
|
|
20
28
|
[result, result[:usage]]
|
|
21
29
|
end
|
|
@@ -27,8 +35,10 @@ module Phronomy
|
|
|
27
35
|
config: {},
|
|
28
36
|
invocation_context: nil,
|
|
29
37
|
on_tool_approval_required: nil,
|
|
30
|
-
on_event: nil
|
|
38
|
+
on_event: nil,
|
|
39
|
+
&block
|
|
31
40
|
)
|
|
41
|
+
listener = resolve_event_listener(on_event, block)
|
|
32
42
|
if invocation_context
|
|
33
43
|
thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
|
|
34
44
|
end
|
|
@@ -40,7 +50,7 @@ module Phronomy
|
|
|
40
50
|
mode: :invoke,
|
|
41
51
|
approval_policy: approval[:policy],
|
|
42
52
|
approval_listener: approval[:listener],
|
|
43
|
-
on_event:
|
|
53
|
+
on_event: listener
|
|
44
54
|
)
|
|
45
55
|
end
|
|
46
56
|
|
|
@@ -4,13 +4,17 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
# Routes Tool work according to the Tool execution contract.
|
|
6
6
|
#
|
|
7
|
+
# Both execution modes return {Phronomy::Task}; only the execution mechanism
|
|
8
|
+
# differs.
|
|
9
|
+
#
|
|
7
10
|
# :cooperative Tool calls execute inline and must return quickly. call_async
|
|
8
11
|
# wraps their result in an already-settled Task and never consumes an
|
|
9
12
|
# OffloadPool worker.
|
|
10
13
|
#
|
|
11
|
-
# :offloaded Tool calls route synchronous work through OffloadPool
|
|
12
|
-
#
|
|
13
|
-
# another long
|
|
14
|
+
# :offloaded Tool calls route synchronous work through OffloadPool, whose
|
|
15
|
+
# caller-facing completion handle is also a Task. Phronomy does not distinguish
|
|
16
|
+
# whether the reason is blocking I/O, CPU-bound work, or another long
|
|
17
|
+
# synchronous operation.
|
|
14
18
|
module ToolExecutor
|
|
15
19
|
def self.call_async(
|
|
16
20
|
tool:,
|