phronomy 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +236 -57
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +500 -411
- data/lib/phronomy/agent/context/capability/base.rb +51 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +42 -6
- data/lib/phronomy/engine/event_loop.rb +269 -112
- data/lib/phronomy/engine/fsm_session.rb +180 -142
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +2 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/tools/mcp.rb +25 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +54 -102
- data/lib/phronomy/workflow_runner.rb +238 -300
- data/lib/phronomy.rb +6 -4
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +18 -7
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -352
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0ea2e317999f3202e06d5afac0b46509fa22a813531919447040d1e048a087f
|
|
4
|
+
data.tar.gz: ae5740df9899b7463b35b54b885948ca1e0e42e01b8b85b1a93fd6437022c90b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 123b0998a7a299f97ce981a3561c68990c41e05decb534b4c4664e441abd6cf368e04ecf6cea61df6a1ea2c4016850b093e0c208aa034fc02b1fa498e0078263
|
|
7
|
+
data.tar.gz: f1ba3ee6163b1a746bd99f00241daf286ac5eb3d8d1e30f7caa739de1de5dd900f3b19b90a063c4a9f19f494a1e9ac1dca59b4d32ade41deabe71f849803ac62
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Workflow#signal(thread_id:, event:, payload:)` for FIFO delivery to a live
|
|
15
|
+
Workflow FSMSession.
|
|
16
|
+
- Workflow transition `action:` callbacks, executed after source exit callbacks
|
|
17
|
+
and before target entry callbacks. Actions may accept `(context)` or
|
|
18
|
+
`(context, event)` and may return a replacement Workflow context.
|
|
19
|
+
- `InvalidAsyncWorkflowActionError` and
|
|
20
|
+
`InvalidAsyncTransitionActionError`. Transition actions may start async work,
|
|
21
|
+
but returning a `Phronomy::Task` is rejected rather than implicitly awaited.
|
|
22
|
+
- Symmetric Agent `on_event:` support for `invoke_async` and `stream_async`;
|
|
23
|
+
streaming differs only by adding `:token` events.
|
|
24
|
+
- `Agent#approve_async` and `Agent::Base.approve_async` resume a suspended
|
|
25
|
+
AgentInvocation without blocking the caller and return a `Phronomy::Task`.
|
|
26
|
+
- Streaming invocations resumed through `approve_async` continue to deliver
|
|
27
|
+
terminal stream events on the Runtime-owned EventLoop thread.
|
|
28
|
+
- `Phronomy::Metrics.snapshot` now reports `event_loop_queue_depth` and
|
|
29
|
+
`event_loop_queue_max_depth`.
|
|
30
|
+
- EventLoop emits a rate-limited warning when its shared event queue reaches
|
|
31
|
+
1,000 pending entries. Events are observed only; they are not dropped.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- Implicit awaiting of Task-returning Workflow/Agent/Tool entry actions and the
|
|
36
|
+
Workflow `action_timeout:` DSL. Entry actions are synchronous RTC callbacks.
|
|
37
|
+
- The duplicate caller-thread `WorkflowRunner#run_workflow` execution path.
|
|
38
|
+
- Agent-wide automatic replay: `Agent::Base.retry_policy` and the `Retryable`
|
|
39
|
+
concern. A failed AgentInvocation is no longer started again by Phronomy.
|
|
40
|
+
- Agent-class `invoke_timeout`. Callers that need a root deadline should pass an
|
|
41
|
+
`InvocationContext` with `deadline:` or `cancellation_token:`.
|
|
42
|
+
- Phronomy LLM operation timeout `config[:llm_timeout]`; configure RubyLLM's
|
|
43
|
+
`request_timeout` instead.
|
|
44
|
+
- Generic Tool retry DSL (`retry_on`, `retry_policies`) and
|
|
45
|
+
`config[:tool_timeout]`; Tool/client implementations own their transport policy.
|
|
46
|
+
- `max_parallel_tools` from Agent, AgentInvocation, ParallelToolChat, and
|
|
47
|
+
InvocationContext.
|
|
48
|
+
- Unused `InvocationContext#provider_limits`.
|
|
49
|
+
- `Configuration#stream_queue_max_size`, which no longer affected the
|
|
50
|
+
Runtime-owned EventLoop streaming path.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- `Workflow#invoke`, `#invoke_async`, and `#stream` now share context
|
|
55
|
+
preparation, StateStore load/save, EventLoop registration, and FSMSession
|
|
56
|
+
execution.
|
|
57
|
+
- Workflow entry and transition action return values use the same
|
|
58
|
+
`set_graph_metadata` context protocol as FSMSession, including duck-typed
|
|
59
|
+
context replacements.
|
|
60
|
+
- Agent terminal outcomes are delivered to `on_event` before the returned Task
|
|
61
|
+
is settled. `Task#on_complete`, `wait_result`, and cancellation remain active.
|
|
62
|
+
- Mapping Agent events to Workflow events, correlation, stale-event handling,
|
|
63
|
+
result persistence, and external Task cancellation are application concerns.
|
|
64
|
+
- LLM transport timeout, transient-error retry, backoff, and jitter are delegated
|
|
65
|
+
to RubyLLM or another configured LLM adapter. Phronomy only translates the
|
|
66
|
+
adapter's final provider error.
|
|
67
|
+
- Agent execution now creates exactly one AgentInvocation session per call.
|
|
68
|
+
- Parallel Tool mode dispatches the complete authorized ToolCall batch; Runtime's
|
|
69
|
+
bounded workers and queues remain the coarse process-protection boundary.
|
|
70
|
+
- Caller-provided deadline and cancellation-token propagation is unchanged.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- `Agent#approve` now rejects EventLoop re-entry instead of synchronously waiting
|
|
75
|
+
for work that can only be dispatched by that same EventLoop.
|
|
76
|
+
|
|
12
77
|
---
|
|
13
78
|
|
|
14
79
|
## [0.14.0] - 2026-07-27
|
data/README.md
CHANGED
|
@@ -27,7 +27,6 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
27
27
|
| Feature | Stability |
|
|
28
28
|
|---|---|
|
|
29
29
|
| **Workflow** — Stateful, branching workflows with wait_state/send_event | Stable |
|
|
30
|
-
| **Workflow action_timeout** — Per-state `action_timeout:` keyword on `state` DSL; cancels Task-returning entry actions that exceed the limit and raises `Phronomy::ActionTimeoutError` | Beta |
|
|
31
30
|
| **Agent** — ReAct-style tool-calling agents with guardrails and conversation history | Stable |
|
|
32
31
|
| **Before-Completion Hook** — Three-tier LLM parameter injection | Stable |
|
|
33
32
|
| **Context Management** — Token budget calculation, estimation, and pruning; `Agent::Base` protected hooks: `build_context` (overridable), `trim_messages`, `trim_to_budget`, `compact_messages`, `budget_exceeded?`, `drop_messages_over` | Stable |
|
|
@@ -37,7 +36,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
37
36
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
38
37
|
| **Eval Framework** — Dataset-driven evaluation with multiple scorer types | Beta |
|
|
39
38
|
| **Tracing** — Pluggable span-based observability | Stable |
|
|
40
|
-
| **Error Taxonomy** — `RateLimitError`, `AuthenticationError`, `ContextLengthError`, `TransportError`
|
|
39
|
+
| **Error Taxonomy** — final RubyLLM/provider errors are translated to `RateLimitError`, `AuthenticationError`, `ContextLengthError`, and `TransportError` without replaying the Agent invocation | Beta |
|
|
41
40
|
|
|
42
41
|
**Knowledge and integration**
|
|
43
42
|
|
|
@@ -49,21 +48,22 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
49
48
|
| **MCP Tool** — `Phronomy::Tools::Mcp`: Model Context Protocol server integration via the official `mcp` gem; `Phronomy::Tools::Agent`: wraps an agent class as a callable tool via `from_agent` | Beta |
|
|
50
49
|
| **Vector Search Tool** — `Phronomy::Tools::VectorSearch`: wraps a `VectorStore` and `Embeddings` adapter as a callable agent tool via `from_store` | Beta |
|
|
51
50
|
|
|
52
|
-
**Execution and reliability**
|
|
53
|
-
|
|
54
|
-
| Feature | Stability |
|
|
51
|
+
| **Execution and reliability** | |
|
|
55
52
|
|---|---|
|
|
56
|
-
| **
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| **`
|
|
53
|
+
| **EventLoop** — Runtime-owned event-driven execution core shared by all Agent invocations, Tool invocations, and Workflow sessions. Not configurable; EventLoop is always active when the Runtime is running. `Phronomy::EventLoop` itself is an internal API | Beta |
|
|
54
|
+
| **`invoke` / `invoke_async`** — `Agent::Base#invoke` blocks the calling thread and returns the final result Hash; `Agent::Base#invoke_async` returns a `Phronomy::Task` immediately without blocking; `Workflow#invoke` and `Workflow#invoke_async` follow the same contract | Stable |
|
|
55
|
+
| **Agent async events** — `invoke_async(..., on_event:)` and `stream_async(..., on_event:)` share `:tool_call`, `:tool_result`, `:approval_required`, `:done`, `:error`, `:timeout`, and `:cancelled`; streaming adds `:token` | Beta |
|
|
56
|
+
| **`stream` / `stream_async`** — callbacks execute on the EventLoop thread and must return quickly; the block form remains a compatibility alias for `on_event:` | Beta |
|
|
57
|
+
| **`stream_callback_error_policy`** — Backward-compatible setting shared by `invoke_async` and `stream_async` terminal `on_event:` callbacks: `:report` (default) preserves the Agent result, while `:fail_task` fails the returned Task with `Phronomy::StreamCallbackError`; Agent execution errors are never replaced by callback errors | Beta |
|
|
58
|
+
| **`invoke_async` / `call_async`** — `Agent::Base#invoke_async` and `Workflow#invoke_async` return a `Task`; `Agent::Context::Capability::Base#call_async` similarly; compatible with EventLoop and standalone contexts | Stable |
|
|
59
|
+
| **`Task#map`** — transforms a Task's completed value and propagates failure/cancellation; Workflow entry and transition actions do not await mapped Tasks | Stable |
|
|
60
60
|
| **CancellationToken** — Cooperative cancellation via `cancel!`/`cancelled?`/`raise_if_cancelled!`; `timeout_after(seconds)` for monotonic-clock deadlines; optional `deadline:` (wall-clock) for backward compatibility; passed as `config: { cancellation_token: token }` to agents and `dispatch_parallel`; injected into `tool.execute` when the method declares a `cancellation_token:` keyword; bridged to `MCP::Cancellation` in `Phronomy::Tools::Mcp#execute` | Experimental |
|
|
61
61
|
| **`dispatch_parallel` / `fan_out` `force_kill:` option** — `force_kill: false` (default) leaves timed-out workers running and raises `TimeoutError` immediately; `force_kill: true` restores the old `Thread#kill` behaviour with a `logger.warn` | Beta |
|
|
62
|
-
| **`execution_mode` DSL on `Agent::Context::Capability::Base`** — Declares how a tool's `execute` should be dispatched: `:cooperative` (same scheduler thread), `:blocking_io` (default; offloaded to `BlockingAdapterPool`), `:cpu_bound`, `:external_process`;
|
|
62
|
+
| **`execution_mode` DSL on `Agent::Context::Capability::Base`** — Declares how a tool's `execute` should be dispatched: `:cooperative` (same scheduler thread), `:blocking_io` (default; offloaded to `BlockingAdapterPool`), `:cpu_bound`, `:external_process`; Tool-specific timeout/retry belongs to the Tool implementation or its client | Experimental |
|
|
63
63
|
| **`blocking_io_pool_size` / `blocking_io_queue_size`** — Configure the default `BlockingAdapterPool` via `Phronomy.configure { \|c\| c.blocking_io_pool_size = 20; c.blocking_io_queue_size = 200 }`; all LLM calls, MCP tool calls, and other blocking I/O share this pool; defaults: `pool_size: 10`, `queue_size: 100` | Beta |
|
|
64
64
|
| **`invocation_context:` keyword on `Agent#invoke` / `Workflow#invoke`** — Pass a `Phronomy::InvocationContext` directly; `thread_id`, `cancellation_token`, and `deadline`-based timeout are derived from it; `task_id` / `parent_task_id` appear in trace spans automatically; `config:` keys remain supported as backward-compat aliases | Beta |
|
|
65
65
|
| **Cooperative scheduler yield points** — `Runtime#yield` (cooperative yield; yields the current task's time slice); `Runtime#yield_if_needed(every: N)` (thread-local counter, yields every N calls); CPU-bound detection when `blocking_detect_threshold_ms` is set (warns and increments `non_yield_threshold_violation_count` when a task runs longer than the threshold without yielding); `starvation_threshold_ms` configuration field (default: 50ms) | Beta |
|
|
66
|
-
| **`Phronomy::Metrics`** — `Phronomy::Metrics.snapshot` returns task-tree and
|
|
66
|
+
| **`Phronomy::Metrics`** — `Phronomy::Metrics.snapshot` returns task-tree, pool, EventLoop, and queue counters; task-centric keys: `active_agent_tasks`, `active_tool_tasks`, `active_workflow_tasks`, `active_llm_tasks`, `task_wait_time_p50_ms`, `task_wait_time_p95_ms`, `task_run_time_p50_ms`, `task_run_time_p95_ms`, `cancelled_tasks`, `failed_tasks`, `non_yield_threshold_violation_count`; EventLoop queue keys: `event_loop_queue_depth` (current pending entries), `event_loop_queue_max_depth` (peak since start); a rate-limited warning is emitted when depth reaches 1,000 — events are not dropped | Beta |
|
|
67
67
|
| **`Phronomy.with_configuration` / `Phronomy.reset_runtime!`** — Scoped configuration override; `reset_runtime!` performs a full `Runtime#shutdown` (including EventLoop termination) then resets configuration; intended for test isolation | Beta |
|
|
68
68
|
| **`Runtime#event_loop`** — Returns the Runtime-owned `EventLoop` instance; lazy-initialised on first access; EventLoop lifetime is tied to the owning Runtime | Beta |
|
|
69
69
|
| **`Runtime#shutdown(timeout:, cancel_grace:)`** — Irreversible Runtime shutdown: drains active sessions, terminates the EventLoop dispatcher, then stops pools and timers; returns a `ShutdownResult` with `runtime_outcome` and `cleanup_status` fields | Beta |
|
|
@@ -75,14 +75,14 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
75
75
|
| **Workflow parallel pattern** — Concurrent branches via application-level threads (no built-in parallel primitive; see the Workflow section for the recommended pattern) | Beta |
|
|
76
76
|
| **Multi-agent** — Agent-as-Tool pattern and hub-and-spoke handoff routing | Beta |
|
|
77
77
|
| **GeneratorVerifier** — Generator-Verifier loop with injectable prompt builders/parsers | Beta |
|
|
78
|
-
|
|
|
79
|
-
|
|
|
78
|
+
| **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
79
|
+
| **`Phronomy::MultiAgent::TeamCoordinator`** — Agent teams pattern: LLM coordinator + stateful workers with sequential task assignment (worker-local message history persisted across tasks) | Beta |
|
|
80
80
|
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
81
|
-
|
|
|
82
|
-
|
|
|
81
|
+
| **Human-in-the-loop approval** — `Agent::Base#invoke` returns `{ suspended: true, agent_invocation_id: String, approval_request: Phronomy::Agent::ToolApprovalRequest }` when a tool requiring approval is encountered; `Agent::Base#approve(id, approval_request_id:, approved:)` (synchronous) or `Agent::Base#approve_async(id, approval_request_id:, approved:)` (returns `Task`) resumes execution; approval state is in-process only — not persisted across process restarts or shared across pods | Beta |
|
|
82
|
+
| **`tool_approval_policy`** — Instance-level callable that maps each `ToolApprovalRequest` to `:allow`, `:require_approval`, or `:reject`; set on the agent instance before invoking | Beta |
|
|
83
83
|
| **`Filter::Base` — unified value filter interface** — `Phronomy::Filter::Base` with a single abstract method `call(value, **context)`; apply to user input (`add_input_filter` / `input_filter` DSL), final LLM output (`add_output_filter` / `output_filter` DSL), or individual tool return values (`add_tool_result_filter(tool_class?, filter)` / `tool_result_filter` DSL); filters transform values and return the result, or raise `Phronomy::FilterBlockError` to reject; filter chains are composable; the same filter instance can be reused across all three sites | Beta |
|
|
84
84
|
|
|
85
|
-
> **Public API boundary**: The
|
|
85
|
+
> **Public API boundary**: The table above lists the primary public features
|
|
86
86
|
> intended for gem consumers. Every entry has an associated stability label.
|
|
87
87
|
> All other classes, modules, and methods — including everything in the
|
|
88
88
|
> [Advanced / Internal APIs](#advanced--internal-apis) section below — are
|
|
@@ -125,10 +125,30 @@ Configure your provider credentials before using agents or chains:
|
|
|
125
125
|
RubyLLM.configure do |c|
|
|
126
126
|
c.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
127
127
|
# c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
128
|
+
|
|
129
|
+
# RubyLLM owns LLM transport timeout and retry policy.
|
|
130
|
+
c.request_timeout = 120
|
|
131
|
+
c.max_retries = 3
|
|
132
|
+
c.retry_interval = 0.1
|
|
133
|
+
c.retry_backoff_factor = 2
|
|
134
|
+
c.retry_interval_randomness = 0.5
|
|
128
135
|
end
|
|
129
136
|
```
|
|
130
137
|
|
|
131
|
-
See the [RubyLLM documentation](https://rubyllm.com) for all supported providers.
|
|
138
|
+
See the [RubyLLM documentation](https://rubyllm.com) for all supported providers. Phronomy does not add another LLM timeout or retry layer.
|
|
139
|
+
|
|
140
|
+
### Execution-policy migration for 0.15
|
|
141
|
+
|
|
142
|
+
| Removed Phronomy setting | Replacement |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `retry_policy` | RubyLLM transport retry, or explicit application orchestration |
|
|
145
|
+
| `invoke_timeout` | `InvocationContext#deadline` or `cancellation_token` when the caller needs a root deadline |
|
|
146
|
+
| `config[:llm_timeout]` | `RubyLLM.configure { |c| c.request_timeout = ... }` |
|
|
147
|
+
| Tool `retry_on` | Tool/client-specific retry with explicit idempotency guarantees |
|
|
148
|
+
| `config[:tool_timeout]` | Tool/client-native timeout |
|
|
149
|
+
| `max_parallel_tools` | No replacement; `parallel_tool_execution` remains an on/off mode |
|
|
150
|
+
| `InvocationContext#provider_limits` | Configure the provider client directly |
|
|
151
|
+
| `stream_queue_max_size` | No replacement; the shared EventLoop queue is unbounded by design. Monitor `Metrics.snapshot[:event_loop_queue_depth]` instead |
|
|
132
152
|
|
|
133
153
|
### Optional dependencies
|
|
134
154
|
|
|
@@ -166,6 +186,75 @@ result = ResearchAgent.new.invoke("What happened in AI research this week?")
|
|
|
166
186
|
puts result[:output]
|
|
167
187
|
```
|
|
168
188
|
|
|
189
|
+
#### Streaming
|
|
190
|
+
|
|
191
|
+
`stream` blocks the calling thread while delivering events; `stream_async` returns a `Task`
|
|
192
|
+
immediately. Callbacks always execute on the **EventLoop thread** — keep them lightweight and
|
|
193
|
+
do not call blocking I/O or synchronous Agent APIs inside a callback.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Synchronous streaming — blocks until done
|
|
197
|
+
result = ResearchAgent.new.stream("What happened in AI research this week?") do |event|
|
|
198
|
+
case event.type
|
|
199
|
+
when :token then print event.payload[:content]
|
|
200
|
+
when :tool_call then puts "\n[Calling: #{event.payload[:tool_call].name}]"
|
|
201
|
+
when :tool_result then puts "[Tool done]"
|
|
202
|
+
when :done then puts "\n---"
|
|
203
|
+
when :approval_required
|
|
204
|
+
# Approval needed — handle via approve_async (see Human-in-the-loop section)
|
|
205
|
+
when :error then warn "Error: #{event.payload[:error].message}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
puts result[:output]
|
|
209
|
+
|
|
210
|
+
# Non-blocking streaming — returns Task immediately
|
|
211
|
+
task = ResearchAgent.new.stream_async("Summarise AI news") do |event|
|
|
212
|
+
broadcast_to_websocket(event) if event.type == :token # must return quickly
|
|
213
|
+
end
|
|
214
|
+
result = task.wait_result
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Human-in-the-loop approval
|
|
218
|
+
|
|
219
|
+
When a tool is configured with `requires_approval true` and no `:allow` policy is set,
|
|
220
|
+
`invoke` suspends and returns `{ suspended: true, agent_invocation_id:, approval_request: }`.
|
|
221
|
+
Resume via `approve` (synchronous) or `approve_async` (non-blocking, safe inside callbacks):
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
agent = ResearchAgent.new
|
|
225
|
+
agent.tool_approval_policy { :require_approval }
|
|
226
|
+
|
|
227
|
+
result = agent.invoke("Run the search")
|
|
228
|
+
if result[:suspended]
|
|
229
|
+
request = result[:approval_request]
|
|
230
|
+
puts "Tool: #{request.items.first.tool_name}"
|
|
231
|
+
|
|
232
|
+
# From top-level code — synchronous
|
|
233
|
+
final = agent.approve(
|
|
234
|
+
result[:agent_invocation_id],
|
|
235
|
+
approval_request_id: request.id,
|
|
236
|
+
approved: true
|
|
237
|
+
)
|
|
238
|
+
puts final[:output]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Inside a stream callback — use approve_async to avoid EventLoop re-entry
|
|
242
|
+
agent.stream_async("Run it") do |event|
|
|
243
|
+
if event.type == :approval_required
|
|
244
|
+
req = event.payload[:request]
|
|
245
|
+
approve_task = agent.approve_async(
|
|
246
|
+
req.agent_invocation_id,
|
|
247
|
+
approval_request_id: req.id,
|
|
248
|
+
approved: true
|
|
249
|
+
)
|
|
250
|
+
# approve_task resolves when the resumed invocation completes
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
> **Important**: Approval state is stored in-process (`AgentInvocationRegistry`). It is **not**
|
|
256
|
+
> persisted across process restarts and is **not** shared between pods or processes.
|
|
257
|
+
|
|
169
258
|
### Workflow — Stateful workflow with wait_state/send_event
|
|
170
259
|
|
|
171
260
|
```ruby runnable
|
|
@@ -203,20 +292,116 @@ final = app.send_event(state: state, event: :approve)
|
|
|
203
292
|
puts "Approved: #{final.approved}" # => true
|
|
204
293
|
```
|
|
205
294
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
which `FSMSession` picks up via the standard `:action_completed` path:
|
|
295
|
+
Start the Agent as an asynchronous activity of the active state, then
|
|
296
|
+
map its lifecycle event to an application-defined Workflow event:
|
|
209
297
|
|
|
210
298
|
```ruby
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
299
|
+
class TranslationContext
|
|
300
|
+
include Phronomy::WorkflowContext
|
|
301
|
+
|
|
302
|
+
field :query
|
|
303
|
+
field :answer
|
|
304
|
+
field :error
|
|
305
|
+
|
|
306
|
+
def handle_fsm_event(event)
|
|
307
|
+
case event.type
|
|
308
|
+
when :translation_completed
|
|
309
|
+
self.answer = event.payload[:answer]
|
|
310
|
+
when :translation_failed
|
|
311
|
+
self.error = event.payload[:error]
|
|
312
|
+
end
|
|
313
|
+
false
|
|
215
314
|
end
|
|
216
|
-
|
|
217
|
-
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
workflow = nil
|
|
318
|
+
|
|
319
|
+
workflow = Phronomy::Workflow.define(TranslationContext) do
|
|
320
|
+
initial :translate
|
|
321
|
+
|
|
322
|
+
state :translate, action: ->(ctx) {
|
|
323
|
+
TranslationAgent.new.invoke_async(
|
|
324
|
+
ctx.query,
|
|
325
|
+
on_event: ->(event) {
|
|
326
|
+
case event.type
|
|
327
|
+
when :done
|
|
328
|
+
workflow.signal(
|
|
329
|
+
thread_id: ctx.thread_id,
|
|
330
|
+
event: :translation_completed,
|
|
331
|
+
payload: {answer: event.payload[:output]}
|
|
332
|
+
)
|
|
333
|
+
when :error, :timeout, :cancelled
|
|
334
|
+
workflow.signal(
|
|
335
|
+
thread_id: ctx.thread_id,
|
|
336
|
+
event: :translation_failed,
|
|
337
|
+
payload: {error: event.payload[:error]}
|
|
338
|
+
)
|
|
339
|
+
end
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
ctx
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
state :done
|
|
346
|
+
state :failed
|
|
347
|
+
|
|
348
|
+
transition from: :translate, on: :translation_completed, to: :done
|
|
349
|
+
transition from: :translate, on: :translation_failed, to: :failed
|
|
350
|
+
end
|
|
218
351
|
```
|
|
219
352
|
|
|
353
|
+
The application owns payload interpretation, correlation, and field updates.
|
|
354
|
+
Phronomy does not automatically copy Agent results into WorkflowContext.
|
|
355
|
+
|
|
356
|
+
Transitions may define an `action:` callback in addition to a `guard:`:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
transition(
|
|
360
|
+
from: :review,
|
|
361
|
+
on: :approved,
|
|
362
|
+
to: :publish,
|
|
363
|
+
guard: ->(context, event) {
|
|
364
|
+
event.payload[:request_id] == context.request_id
|
|
365
|
+
},
|
|
366
|
+
action: ->(context, event) {
|
|
367
|
+
context.merge(approved_by: event.payload[:reviewer])
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The callback order for a successful transition is:
|
|
373
|
+
|
|
374
|
+
```text
|
|
375
|
+
source exit callbacks
|
|
376
|
+
-> selected transition action
|
|
377
|
+
-> target entry callbacks
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Transition actions may accept either `(context)` or `(context, event)`. A
|
|
381
|
+
returned Workflow context replaces the current context before target entry
|
|
382
|
+
callbacks run. Returning `nil` or another non-context value preserves the
|
|
383
|
+
current context.
|
|
384
|
+
|
|
385
|
+
Like entry actions, transition actions are synchronous Run-to-Completion
|
|
386
|
+
callbacks. They may start asynchronous work and register a listener that later
|
|
387
|
+
calls `Workflow#signal`, but they must return the context or `nil` immediately.
|
|
388
|
+
Returning `Phronomy::Task` raises
|
|
389
|
+
`Phronomy::InvalidAsyncTransitionActionError`; Phronomy does not implicitly
|
|
390
|
+
await it.
|
|
391
|
+
|
|
392
|
+
For a transition with `on:`, the two-argument action receives the external
|
|
393
|
+
`Phronomy::Event`. For an automatic transition without `on:`, it receives the
|
|
394
|
+
internal event:
|
|
395
|
+
|
|
396
|
+
```text
|
|
397
|
+
event.type == :state_completed
|
|
398
|
+
event.payload == nil
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
When several transitions have the same source and event, guards are evaluated
|
|
402
|
+
in declaration order. The first matching transition is selected, and only that
|
|
403
|
+
transition's action runs.
|
|
404
|
+
|
|
220
405
|
### Multi-Agent — Agent-as-Tool pattern
|
|
221
406
|
|
|
222
407
|
Wrap sub-agents as `Agent::Context::Capability::Base` subclasses so the orchestrator LLM can call them on demand.
|
|
@@ -403,7 +588,7 @@ rescue Phronomy::LowConfidenceError => e
|
|
|
403
588
|
end
|
|
404
589
|
```
|
|
405
590
|
|
|
406
|
-
###
|
|
591
|
+
### MultiAgent::Orchestrator — Parallel subagent dispatch
|
|
407
592
|
|
|
408
593
|
> **Note:** `dispatch_parallel` and `fan_out` use plain Ruby threads. Use
|
|
409
594
|
> `max_concurrency:` to cap the number of concurrent workers and `on_error:`
|
|
@@ -412,7 +597,7 @@ end
|
|
|
412
597
|
> fan-outs consider additional rate-limiting at the application level.
|
|
413
598
|
|
|
414
599
|
```ruby
|
|
415
|
-
class ResearchOrchestrator < Phronomy::
|
|
600
|
+
class ResearchOrchestrator < Phronomy::MultiAgent::Orchestrator
|
|
416
601
|
model "gpt-4o"
|
|
417
602
|
instructions "Coordinate research tasks by dispatching to specialised agents."
|
|
418
603
|
|
|
@@ -427,7 +612,7 @@ result = ResearchOrchestrator.new.invoke("Research the latest AI news.")
|
|
|
427
612
|
Programmatic parallel dispatch (no LLM loop):
|
|
428
613
|
|
|
429
614
|
```ruby
|
|
430
|
-
class MyOrchestrator < Phronomy::
|
|
615
|
+
class MyOrchestrator < Phronomy::MultiAgent::Orchestrator
|
|
431
616
|
model "gpt-4o"
|
|
432
617
|
instructions "Orchestrate."
|
|
433
618
|
|
|
@@ -573,7 +758,7 @@ puts result2[:output] # => "Your name is Alice."
|
|
|
573
758
|
Persist it however suits your application (in-memory hash, Redis, ActiveRecord, etc.).
|
|
574
759
|
|
|
575
760
|
> **Note on `thread_id`**: `thread_id` is a correlation identifier used internally for
|
|
576
|
-
>
|
|
761
|
+
> EventLoop session routing and compaction context. It does **not** automatically persist or
|
|
577
762
|
> restore conversation history — you must pass `messages:` explicitly on each turn as shown above.
|
|
578
763
|
|
|
579
764
|
|
|
@@ -587,14 +772,16 @@ Phronomy.configure do |c|
|
|
|
587
772
|
c.before_completion = nil # optional; global hook lambda
|
|
588
773
|
c.trace_pii = false # default; set to true only when trace data contains no PII
|
|
589
774
|
c.logger = nil # optional; any object responding to #warn (e.g. Rails.logger)
|
|
590
|
-
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on
|
|
775
|
+
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on shutdown
|
|
591
776
|
c.runtime_backend = :thread # :thread (default); :immediate (tests, synchronous); :fiber (experimental validation only); :cooperative (deprecated alias for :immediate)
|
|
592
|
-
c.strict_runtime_guards = false
|
|
777
|
+
c.strict_runtime_guards = false # when true, raises SchedulerReentrancyError on invoke-inside-task
|
|
778
|
+
c.stream_callback_error_policy = :report # :report (default) preserves Agent result; :fail_task fails Task with StreamCallbackError
|
|
593
779
|
end
|
|
594
780
|
```
|
|
595
781
|
|
|
596
782
|
`c.logger` receives framework diagnostic messages (e.g. unreachable-state warnings from
|
|
597
|
-
`Workflow.define
|
|
783
|
+
`Workflow.define`, stream callback errors, EventLoop queue backlog warnings). When `nil`
|
|
784
|
+
(default), messages are written to `$stderr` via `Kernel#warn`.
|
|
598
785
|
|
|
599
786
|
> **Note**: When `trace_pii = false`, both the _input_ and the _output_ (LLM
|
|
600
787
|
> responses and tool results) are replaced with `[REDACTED]` in trace spans.
|
|
@@ -611,6 +798,10 @@ Understanding when to use each prevents scheduler stalls and hidden deadlocks.
|
|
|
611
798
|
| Top-level application code, Rails controller, background job | `agent.invoke(input)` — blocks the calling thread until done |
|
|
612
799
|
| Workflow action / EventLoop callback | `agent.invoke_async(input).map { |r| ctx.merge(output: r[:output]) }` — returns a Task and resumes by state transition |
|
|
613
800
|
| Top-level code that wants explicit async | `agent.invoke_async(input).wait_result` — blocks the calling thread until the Task completes |
|
|
801
|
+
| Streaming from top-level code | `agent.stream(input) { |event| ... }` — blocks until done; callbacks run on the EventLoop thread |
|
|
802
|
+
| Streaming non-blocking | `task = agent.stream_async(input) { |event| ... }` — returns Task immediately; callbacks run on the EventLoop thread |
|
|
803
|
+
| Resume approval from top-level code | `agent.approve(id, approval_request_id: r_id, approved: true)` — synchronous; blocks until resumed |
|
|
804
|
+
| Resume approval from an EventLoop callback | `agent.approve_async(id, approval_request_id: r_id, approved: true)` — returns Task; safe to call from a stream callback |
|
|
614
805
|
|
|
615
806
|
### Why this matters
|
|
616
807
|
|
|
@@ -706,8 +897,7 @@ class MyAgent < Phronomy::Agent::Base
|
|
|
706
897
|
model "gpt-4o"
|
|
707
898
|
max_output_tokens 4096 # override max_output_tokens from registry
|
|
708
899
|
context_overhead 600 # extra reservation for system prompt + tools
|
|
709
|
-
|
|
710
|
-
max_parallel_tools 4 # cap concurrent tool executions (default: 10)
|
|
900
|
+
# LLM timeout/retry is configured on RubyLLM, not on the Agent class.
|
|
711
901
|
end
|
|
712
902
|
```
|
|
713
903
|
|
|
@@ -733,7 +923,7 @@ Pass a `CancellationToken` to any agent via `config: { cancellation_token: token
|
|
|
733
923
|
Cancellation is checked at multiple granular checkpoints: before the LLM call,
|
|
734
924
|
after each streaming chunk, before each parallel
|
|
735
925
|
tool-call batch, and after each `before_completion` hook. `CancellationError` is
|
|
736
|
-
raised immediately
|
|
926
|
+
raised immediately. Phronomy does not replay the complete Agent invocation. No threads are force-killed — `ensure`
|
|
737
927
|
blocks always execute.
|
|
738
928
|
|
|
739
929
|
> **Cooperative cancellation — not preemptive**
|
|
@@ -775,30 +965,19 @@ blocks always execute.
|
|
|
775
965
|
> which calls `cancel!` on expiry and fires all `on_cancel` callbacks — including
|
|
776
966
|
> the MCP bridge.
|
|
777
967
|
|
|
778
|
-
>
|
|
779
|
-
> worker termination**
|
|
968
|
+
> **Transport timeout and retry ownership**
|
|
780
969
|
>
|
|
781
|
-
>
|
|
782
|
-
>
|
|
783
|
-
>
|
|
970
|
+
> Phronomy does not interpret `config[:llm_timeout]`, `config[:tool_timeout]`,
|
|
971
|
+
> Agent `retry_policy`, or Tool `retry_on`. Configure LLM transport behavior on
|
|
972
|
+
> RubyLLM (or another adapter) and configure Tool transport behavior on the Tool's
|
|
973
|
+
> HTTP/DB/MCP client. This ensures the layer capable of safely aborting the I/O owns
|
|
974
|
+
> the timeout and retry semantics.
|
|
784
975
|
>
|
|
785
|
-
>
|
|
976
|
+
> `InvocationContext#deadline` and `cancellation_token` remain available for a
|
|
977
|
+
> caller-defined root-operation boundary. They provide cooperative cancellation
|
|
978
|
+
> across the Phronomy execution tree; they do not replace provider-native socket,
|
|
979
|
+
> request, statement, or session timeouts.
|
|
786
980
|
>
|
|
787
|
-
> | Situation | `TimeoutError`? | `abandoned?` | `abandoned_count` |
|
|
788
|
-
> |---|---|---|---|
|
|
789
|
-
> | Deadline fires while worker is executing | ✅ via `on_complete` | `true` | +1 |
|
|
790
|
-
> | Deadline fires while op is still queued | ✅ via `on_complete` | `false` | unchanged |
|
|
791
|
-
> | `blocking_wait(timeout:)` expires | ✅ to that waiter only | unchanged | unchanged |
|
|
792
|
-
> | `CancellationToken` cancelled | `CancellationError` | — | — |
|
|
793
|
-
> | Streaming path | not guaranteed (separate fix needed) | — | — |
|
|
794
|
-
>
|
|
795
|
-
> `blocking_wait(timeout:)` is a **waiter-local** deadline — the operation remains
|
|
796
|
-
> unsettled and other waiters or `on_complete` callbacks will still receive the
|
|
797
|
-
> eventual result (unless a submit-time deadline also fires).
|
|
798
|
-
>
|
|
799
|
-
> Size `blocking_io_pool_size` to account for the worst-case number of
|
|
800
|
-
> concurrently abandoned workers that may accumulate before the pool is saturated.
|
|
801
|
-
|
|
802
981
|
```ruby
|
|
803
982
|
token = Phronomy::Concurrency::CancellationToken.new
|
|
804
983
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Scenarios:
|
|
10
10
|
# 1. Minimal agent (no tools, no knowledge) — baseline framework overhead.
|
|
11
|
-
# 2. Tool-aware agent with
|
|
11
|
+
# 2. Tool-aware agent with a registered stub Tool.
|
|
12
12
|
# 3. Agent#stream setup latency (first-chunk time with stubbed stream).
|
|
13
13
|
|
|
14
14
|
require "benchmark"
|
|
@@ -79,7 +79,6 @@ end
|
|
|
79
79
|
bench_tool_class = Class.new(Phronomy::Agent::Base) do
|
|
80
80
|
model "stub-model"
|
|
81
81
|
tools BenchNullTool
|
|
82
|
-
max_parallel_tools 4
|
|
83
82
|
|
|
84
83
|
define_method(:build_chat) { |*| BenchStubChat.new(BENCH_RESP) }
|
|
85
84
|
end
|
|
@@ -97,7 +96,7 @@ Benchmark.bm(50) do |x|
|
|
|
97
96
|
end
|
|
98
97
|
end
|
|
99
98
|
|
|
100
|
-
x.report("Agent#invoke —
|
|
99
|
+
x.report("Agent#invoke — tool-aware, #{AGENT_INVOKE_ITERATIONS} iters") do
|
|
101
100
|
AGENT_INVOKE_ITERATIONS.times do
|
|
102
101
|
BENCH_AGENT_TOOLS.invoke("ping", thread_id: "bench-#{rand(1_000_000)}")
|
|
103
102
|
end
|
|
@@ -2,74 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Superseded by [ADR-011](011-delegate-transport-policy-to-adapters.md).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Historical decision
|
|
8
8
|
|
|
9
|
-
`Agent::Base
|
|
10
|
-
|
|
9
|
+
Phronomy previously exposed `Agent::Base.invoke_timeout` as a class-level wait
|
|
10
|
+
boundary. The initial implementation did not stop background work; a later
|
|
11
|
+
implementation attached a cancellation scope to the complete Agent invocation.
|
|
11
12
|
|
|
12
|
-
The
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
same hazards because it can interrupt anywhere inside a `rescue`/`ensure` block.
|
|
18
|
-
|
|
19
|
-
Cooperative cancellation (checking a shared flag periodically) is safe but
|
|
20
|
-
requires every tool, every LLM call, and every framework-internal loop to
|
|
21
|
-
participate — a significant API surface change.
|
|
22
|
-
|
|
23
|
-
## Decision
|
|
24
|
-
|
|
25
|
-
`invoke_timeout` is a **wait timeout only**. When the deadline is reached:
|
|
26
|
-
|
|
27
|
-
- `TimeoutError` is raised in the calling thread.
|
|
28
|
-
- The agent's background thread continues running until it either completes
|
|
29
|
-
normally or is garbage-collected when the process ends.
|
|
30
|
-
- No cancellation signal is sent to the agent.
|
|
31
|
-
|
|
32
|
-
This is explicitly documented in the README and in the DSL source.
|
|
33
|
-
|
|
34
|
-
A proper cooperative cancellation mechanism is tracked in Issue #216
|
|
35
|
-
(`CancellationToken`), which is a separate feature requiring agent, tool, and
|
|
36
|
-
transport layer participation.
|
|
37
|
-
|
|
38
|
-
## Consequences
|
|
39
|
-
|
|
40
|
-
**Positive:**
|
|
41
|
-
- No risk of leaving shared resources (DB connections, mutexes, sockets) in a
|
|
42
|
-
broken state due to forced thread interruption.
|
|
43
|
-
- Implementation is simple: `Timeout.timeout` on the calling side only.
|
|
44
|
-
- The contract is explicit and predictable.
|
|
45
|
-
|
|
46
|
-
**Negative / Tradeoffs:**
|
|
47
|
-
- Background threads may continue consuming resources (LLM API quota, etc.)
|
|
48
|
-
after the caller has given up.
|
|
49
|
-
- Users who expect "cancel" semantics from a timeout will be surprised.
|
|
50
|
-
- Proper cancellation requires the `CancellationToken` feature (#216), which
|
|
51
|
-
has not yet been implemented.
|
|
52
|
-
|
|
53
|
-
## Extension: PendingOperation#await cooperative cancellation semantics
|
|
54
|
-
|
|
55
|
-
`BlockingAdapterPool::PendingOperation#await` also supports both `timeout:` and
|
|
56
|
-
`cancellation_token:` parameters. The same non-preemptive rule applies here,
|
|
57
|
-
consistent with ADR-010 (cooperative-first, non-preemptive concurrency model):
|
|
58
|
-
|
|
59
|
-
1. **No forcible thread termination.** When a `cancellation_token` is cancelled,
|
|
60
|
-
`CancellationError` is raised to the `await` caller; when the timeout fires,
|
|
61
|
-
`TimeoutError` is raised instead. In both cases, the underlying worker thread
|
|
62
|
-
is **not** killed. The worker runs its block to natural completion.
|
|
63
|
-
2. **Cooperative, not preemptive.** Cancellation takes effect only at `await`
|
|
64
|
-
call sites or at explicit `token.check!` checkpoints inside the submitted
|
|
65
|
-
block. Code that ignores the token will not be interrupted.
|
|
66
|
-
3. **Timeout scope.** `timeout:` at `await` time is measured from the moment
|
|
67
|
-
`await` is called. If both submit-time and await-time timeouts are provided,
|
|
68
|
-
the earlier deadline wins.
|
|
69
|
-
4. **Error propagation.** `CancellationError` (or `TimeoutError`) is raised to
|
|
70
|
-
the `await` caller; the submitter is responsible for handling it.
|
|
71
|
-
|
|
72
|
-
These semantics are identical in spirit to the `invoke_timeout` decision above:
|
|
73
|
-
the framework exposes a *wait* boundary, not a hard-kill boundary. Safe resource
|
|
74
|
-
cleanup is the caller's responsibility.
|
|
13
|
+
The API has been removed. An Agent class no longer owns a default invocation
|
|
14
|
+
timeout. Applications that need a deadline for a particular root operation pass
|
|
15
|
+
an `InvocationContext` with `deadline:` or `cancellation_token:`. Those values are
|
|
16
|
+
coordination context supplied by the caller, not an implicit Agent execution
|
|
17
|
+
policy.
|
|
75
18
|
|
|
19
|
+
LLM transport timeout belongs to RubyLLM or another configured LLM adapter.
|
|
20
|
+
Tool transport timeout belongs to the Tool implementation or its underlying
|
|
21
|
+
client. Phronomy retains generic deadline and cancellation primitives for the
|
|
22
|
+
execution tree it coordinates.
|