phronomy 0.15.0 → 0.16.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 +107 -18
- data/README.md +300 -75
- data/benchmark/bench_agent_invoke.rb +3 -0
- data/benchmark/bench_regression.rb +2 -18
- data/benchmark/bench_tool_schema.rb +1 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
- data/lib/phronomy/agent/activation_registry.rb +28 -0
- data/lib/phronomy/agent/agent_execution.rb +97 -0
- data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
- data/lib/phronomy/agent/agent_invocation.rb +42 -10
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
- data/lib/phronomy/agent/agent_root.rb +67 -0
- data/lib/phronomy/agent/async_event_api.rb +55 -393
- data/lib/phronomy/agent/base.rb +301 -641
- data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
- data/lib/phronomy/agent/context_assembler.rb +321 -0
- data/lib/phronomy/agent/context_candidate.rb +47 -0
- data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
- data/lib/phronomy/agent/context_importer.rb +217 -0
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +188 -0
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
- data/lib/phronomy/agent/context_plan.rb +25 -0
- data/lib/phronomy/agent/context_plan_validator.rb +167 -0
- data/lib/phronomy/agent/context_policies/default.rb +53 -0
- data/lib/phronomy/agent/context_policy.rb +15 -0
- data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
- data/lib/phronomy/agent/context_policy_registry.rb +46 -0
- data/lib/phronomy/agent/context_request.rb +35 -0
- data/lib/phronomy/agent/context_selection_unit.rb +38 -0
- data/lib/phronomy/agent/derived_content_spec.rb +34 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1123 -0
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
- data/lib/phronomy/agent/immutable.rb +31 -0
- data/lib/phronomy/agent/journal_projection.rb +34 -0
- data/lib/phronomy/agent/journal_record.rb +67 -0
- data/lib/phronomy/agent/llm_call_record.rb +51 -0
- data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
- data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
- data/lib/phronomy/agent/llm_input_patch.rb +21 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
- data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +298 -0
- data/lib/phronomy/agent/token_budget_resolver.rb +69 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
- data/lib/phronomy/agent/tool_definition_set.rb +55 -0
- data/lib/phronomy/agent.rb +14 -16
- data/lib/phronomy/agent_busy_error.rb +5 -0
- data/lib/phronomy/canonical_json.rb +136 -0
- data/lib/phronomy/configuration.rb +9 -4
- data/lib/phronomy/content_store/base.rb +51 -0
- data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
- data/lib/phronomy/engine/event_loop.rb +3 -0
- data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
- data/lib/phronomy/llm_context_window/assembler.rb +8 -8
- data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
- data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
- data/lib/phronomy/persistence/in_memory.rb +247 -0
- data/lib/phronomy/persistence.rb +39 -0
- data/lib/phronomy/tools/agent.rb +14 -36
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +11 -0
- data/scripts/add_to_h_to_token_doubles.rb +33 -0
- data/scripts/add_to_h_unnamed_doubles.rb +27 -0
- data/scripts/migrate_spec_agent_definition.rb +108 -0
- data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
- data/scripts/migrate_spec_inline_pass3.rb +24 -0
- metadata +54 -47
- data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
- data/lib/phronomy/agent/before_completion_context.rb +0 -47
- data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 633f62428045fed9447fd95414687cf8f2d0df11163afd767a73cc1c9b0ea97e
|
|
4
|
+
data.tar.gz: 68d352c78047e7b87dade2f8659cf44d4ecd16acf506090329d3e276f8a35f0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67cd757c8efacd34360a3b876bae0ebbac0e37080f77472a9b69eaf774f14b5f2c070288d4e5d66bbfcc31a6be60353ca043e4f95ee6b1a14da09e8fe5792b00
|
|
7
|
+
data.tar.gz: '0382e7bbf708605dea900725e7ab4c093baf653ef0e88c11ba79c6ab227b1584624aa73b9b16cacd8644d4d1aafe0ffade1b6b1d6361a1f33d7395e730b4e1df'
|
data/CHANGELOG.md
CHANGED
|
@@ -9,8 +9,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.16.0] - 2026-08-08
|
|
15
|
+
|
|
12
16
|
### Added
|
|
13
17
|
|
|
18
|
+
- Stateful Agent identity and persistence:
|
|
19
|
+
- Every concrete Agent definition declares a stable `agent_definition id:, version:`.
|
|
20
|
+
- Every Agent instance has a stable `agent_id`.
|
|
21
|
+
- `Agent::Base.create` creates a persistent Agent instance.
|
|
22
|
+
- `Agent::Base.load` restores an Agent from a shared Persistence backend.
|
|
23
|
+
- `Agent#agent_root`, `#transcript`, `#clear_transcript!`, `#clear_memory!`,
|
|
24
|
+
`#reset_context!`, `#close!`, and `#purge!` provide explicit state lifecycle operations.
|
|
25
|
+
|
|
26
|
+
- Persistence-backed Agent execution state:
|
|
27
|
+
- Agent executions have stable `execution_id` values and are persisted separately from the owning Agent.
|
|
28
|
+
- Suspended approval executions remain represented in Persistence.
|
|
29
|
+
- Resuming after process loss still requires future durable activation rehydration support.
|
|
30
|
+
|
|
31
|
+
- Canonical Complete Execution Log:
|
|
32
|
+
- Phronomy records observed logical execution facts in an append-only Agent Journal.
|
|
33
|
+
- Provider assistant responses preserve assistant content and all Tool Calls as one logical assistant message.
|
|
34
|
+
- Raw Tool return values and the Tool-role messages sent back to the LLM are represented as separate execution facts.
|
|
35
|
+
|
|
36
|
+
- Per-LLM-call canonical Manifests:
|
|
37
|
+
- Each LLM Call is assembled from a canonical Manifest.
|
|
38
|
+
- The Manifest records the logical input selected for that specific LLM Call.
|
|
39
|
+
- Runtime RubyLLM messages are materialized from the Manifest rather than treated as the source of truth.
|
|
40
|
+
|
|
41
|
+
- Context Policy domain:
|
|
42
|
+
- Context candidates are selected from canonical history without deleting the underlying Journal.
|
|
43
|
+
- Tool Call / Tool message protocol dependencies are selected atomically.
|
|
44
|
+
- Required context is validated independently from optional historical context.
|
|
45
|
+
- `ContextBudgetExceededError` is raised when required context cannot fit in the available model budget.
|
|
46
|
+
|
|
47
|
+
- Context import for stateful Agents:
|
|
48
|
+
- Existing user / assistant / Tool history can be supplied through Agent creation context.
|
|
49
|
+
- Imported assistant messages retain their original logical message boundary and Tool Calls.
|
|
50
|
+
- Invalid Tool protocol histories are rejected instead of being guessed or repaired.
|
|
51
|
+
|
|
14
52
|
- `Workflow#signal(thread_id:, event:, payload:)` for FIFO delivery to a live
|
|
15
53
|
Workflow FSMSession.
|
|
16
54
|
- Workflow transition `action:` callbacks, executed after source exit callbacks
|
|
@@ -30,27 +68,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
30
68
|
- EventLoop emits a rate-limited warning when its shared event queue reaches
|
|
31
69
|
1,000 pending entries. Events are observed only; they are not dropped.
|
|
32
70
|
|
|
33
|
-
###
|
|
71
|
+
### Changed
|
|
34
72
|
|
|
35
|
-
-
|
|
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.
|
|
73
|
+
- Agent instances are now always stateful and Persistence-backed.
|
|
51
74
|
|
|
52
|
-
|
|
75
|
+
- Conversation history ownership has moved from the caller to the Agent:
|
|
76
|
+
callers no longer need to pass the previous `messages` array back on every invocation.
|
|
77
|
+
Completed invocation results may still expose `result[:messages]` as a materialized
|
|
78
|
+
transcript projection.
|
|
79
|
+
|
|
80
|
+
- `agent_definition id:, version:` is required for concrete Agent definitions.
|
|
81
|
+
Loading persisted Agent state validates the stored definition identity and version
|
|
82
|
+
against the runtime Agent class.
|
|
83
|
+
|
|
84
|
+
- Context-window management is now Manifest-first:
|
|
85
|
+
canonical Agent history is retained in the Journal while each LLM Call receives
|
|
86
|
+
only the context selected for its Manifest.
|
|
87
|
+
|
|
88
|
+
- Context pruning no longer means deleting or mutating historical Agent messages.
|
|
89
|
+
Context Policy omission affects only the current LLM Call input.
|
|
53
90
|
|
|
91
|
+
- Tool execution results and Tool protocol messages are no longer treated as the
|
|
92
|
+
same value. The raw Tool return value is retained as an execution fact while the
|
|
93
|
+
exact Tool-role message remains independently available for LLM context assembly.
|
|
94
|
+
|
|
95
|
+
- `thread_id` is an execution/correlation identifier rather than the owner of
|
|
96
|
+
conversation state. Persistent Agent identity is defined by `agent_id`.
|
|
97
|
+
|
|
98
|
+
- `context_overhead` is retained only for the legacy `build_context` path.
|
|
99
|
+
Manifest-first context assembly accounts for actual mandatory context instead of
|
|
100
|
+
reserving this value as Tool/system-prompt overhead.
|
|
101
|
+
|
|
102
|
+
- Phronomy now requires `ruby_llm >= 1.15, < 2` so Provider assistant messages can
|
|
103
|
+
be captured before Agent-owned Tool execution begins.
|
|
104
|
+
|
|
105
|
+
- Refactor: `Agent::AsyncEventApi` is now the single implementation of
|
|
106
|
+
`invoke`, `invoke_async`, `stream`, `stream_async`, and their session
|
|
107
|
+
lifecycle helpers (`_start_invocation`, `_handle_agent_completion`,
|
|
108
|
+
`_register_tool_invocation_session`, `_start_approval_resume`). The
|
|
109
|
+
duplicate definitions in `Agent::Base` have been removed. No public
|
|
110
|
+
behavior change is intended.
|
|
111
|
+
- Remove `faraday` and `event_stream_parser` from gemspec declared
|
|
112
|
+
dependencies; both are transitive dependencies of `ruby_llm` and are
|
|
113
|
+
not used directly by phronomy.
|
|
54
114
|
- `Workflow#invoke`, `#invoke_async`, and `#stream` now share context
|
|
55
115
|
preparation, StateStore load/save, EventLoop registration, and FSMSession
|
|
56
116
|
execution.
|
|
@@ -69,6 +129,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
69
129
|
bounded workers and queues remain the coarse process-protection boundary.
|
|
70
130
|
- Caller-provided deadline and cancellation-token propagation is unchanged.
|
|
71
131
|
|
|
132
|
+
### Removed
|
|
133
|
+
|
|
134
|
+
- Caller-managed `messages:` continuation from the Agent invocation API.
|
|
135
|
+
Stateful Agent history is now obtained from the Agent's persisted Journal.
|
|
136
|
+
|
|
137
|
+
- `Agent::Base#trim_messages` and the legacy Agent-level message-trimming model.
|
|
138
|
+
Context selection is performed by the Context Policy / Manifest assembly path.
|
|
139
|
+
|
|
140
|
+
- The assumption that `build_context` and the legacy `LlmContextWindow::Assembler`
|
|
141
|
+
are the long-term single authority for LLM input. ADR-012 defines the replacement
|
|
142
|
+
Journal / Context Policy / Manifest architecture.
|
|
143
|
+
|
|
144
|
+
- Implicit awaiting of Task-returning Workflow/Agent/Tool entry actions and the
|
|
145
|
+
Workflow `action_timeout:` DSL. Entry actions are synchronous RTC callbacks.
|
|
146
|
+
- The duplicate caller-thread `WorkflowRunner#run_workflow` execution path.
|
|
147
|
+
- Agent-wide automatic replay: `Agent::Base.retry_policy` and the `Retryable`
|
|
148
|
+
concern. A failed AgentInvocation is no longer started again by Phronomy.
|
|
149
|
+
- Agent-class `invoke_timeout`. Callers that need a root deadline should pass an
|
|
150
|
+
`InvocationContext` with `deadline:` or `cancellation_token:`.
|
|
151
|
+
- Phronomy LLM operation timeout `config[:llm_timeout]`; configure RubyLLM's
|
|
152
|
+
`request_timeout` instead.
|
|
153
|
+
- Generic Tool retry DSL (`retry_on`, `retry_policies`) and
|
|
154
|
+
`config[:tool_timeout]`; Tool/client implementations own their transport policy.
|
|
155
|
+
- `max_parallel_tools` from Agent, AgentInvocation, ParallelToolChat, and
|
|
156
|
+
InvocationContext.
|
|
157
|
+
- Unused `InvocationContext#provider_limits`.
|
|
158
|
+
- `Configuration#stream_queue_max_size`, which no longer affected the
|
|
159
|
+
Runtime-owned EventLoop streaming path.
|
|
160
|
+
|
|
72
161
|
### Fixed
|
|
73
162
|
|
|
74
163
|
- `Agent#approve` now rejects EventLoop re-entry instead of synchronously waiting
|
data/README.md
CHANGED
|
@@ -27,9 +27,9 @@ 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
|
-
| **Agent** — ReAct-style tool-calling agents with guardrails and conversation
|
|
31
|
-
| **Before-
|
|
32
|
-
| **Context Management** —
|
|
30
|
+
| **Agent** — Stateful ReAct-style tool-calling agents with stable `agent_id`, persistence-backed execution state, canonical execution history, guardrails, and conversation context | Stable |
|
|
31
|
+
| **Before-LLM-Input Hook** — Three-tier per-call LLM input customization via `before_llm_input` and `LLMInputPatch` | Stable |
|
|
32
|
+
| **Context Management** — Canonical Journal + per-LLM-call Manifest architecture with token-budget-aware selection and protocol-safe Tool Call / Tool message dependencies. Context selection never deletes canonical execution history | Stable |
|
|
33
33
|
| **Filters** — Input/output transformation and blocking via `Filter::Base`; call `block!(reason)` to reject and raise `FilterBlockError` | Beta |
|
|
34
34
|
| **`PromptInjectionFilter`** — Built-in `Filter::Base` subclass that detects prompt-injection patterns; usable standalone or as part of a filter chain | Beta |
|
|
35
35
|
| **`Agent::Context::Capability::Base.redact_params` / `.max_result_size`** — Class-level DSL: `redact_params` masks parameter values in log/trace output; `max_result_size` truncates oversized tool results before they reach the LLM | Beta |
|
|
@@ -56,7 +56,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
56
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
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
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
|
|
59
|
+
| **`Task#map`** — transforms a Task's completed value and propagates failure/cancellation. `Task#map` remains available for application-level Task composition, but Workflow entry and transition actions must not return a Task | 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
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 |
|
|
@@ -78,7 +78,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
78
78
|
| **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
79
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
|
-
| **Human-in-the-loop approval** — `Agent::Base#invoke` returns `{ suspended: true,
|
|
81
|
+
| **Human-in-the-loop approval** — `Agent::Base#invoke` returns `{ suspended: true, execution_id: String, approval_request: Phronomy::Agent::ToolApprovalRequest }` when approval is required. `#approve` / `#approve_async` resume that execution. Suspended execution state is stored in Persistence, but durable activation rehydration after a process restart is not yet supported | Beta |
|
|
82
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
|
|
|
@@ -176,6 +176,7 @@ class WebSearch < Phronomy::Agent::Context::Capability::Base
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
class ResearchAgent < Phronomy::Agent::Base
|
|
179
|
+
agent_definition id: "research-agent", version: 1
|
|
179
180
|
model "gpt-4o"
|
|
180
181
|
instructions "You are a research assistant. Use tools to answer questions."
|
|
181
182
|
tools WebSearch
|
|
@@ -519,26 +520,106 @@ puts result[:output] # final answer
|
|
|
519
520
|
puts result[:agent].class # => BillingAgent
|
|
520
521
|
```
|
|
521
522
|
|
|
522
|
-
### Before-
|
|
523
|
+
### Before-LLM-Input Hook — Per-call LLM input customization
|
|
524
|
+
|
|
525
|
+
`before_llm_input` runs before every LLM call and allows an application to
|
|
526
|
+
customize that call without mutating the Agent, RubyLLM chat, or canonical
|
|
527
|
+
Journal state directly.
|
|
528
|
+
|
|
529
|
+
Hooks can be configured at three levels:
|
|
530
|
+
|
|
531
|
+
1. global — applies to every Agent
|
|
532
|
+
2. class — applies to every instance of one Agent definition
|
|
533
|
+
3. instance — applies only to one Agent instance
|
|
534
|
+
|
|
535
|
+
They run in that order: global → class → instance.
|
|
536
|
+
|
|
537
|
+
A hook receives an immutable `Phronomy::Agent::LLMInputBuildContext` containing
|
|
538
|
+
metadata about the LLM call:
|
|
539
|
+
|
|
540
|
+
- `agent_id`
|
|
541
|
+
- `agent_definition_id`
|
|
542
|
+
- `definition_version`
|
|
543
|
+
- `config`
|
|
544
|
+
- `call_sequence`
|
|
545
|
+
|
|
546
|
+
The hook does not receive the mutable Agent instance, RubyLLM messages, or
|
|
547
|
+
`RubyLLM::Chat`.
|
|
548
|
+
|
|
549
|
+
Return either:
|
|
550
|
+
|
|
551
|
+
- `nil` to leave the call unchanged, or
|
|
552
|
+
- a `Phronomy::Agent::LLMInputPatch`
|
|
553
|
+
|
|
554
|
+
### Class-level hook
|
|
523
555
|
|
|
524
556
|
```ruby
|
|
525
|
-
# Class-level: applies to all instances
|
|
526
557
|
class MyAgent < Phronomy::Agent::Base
|
|
558
|
+
agent_definition id: "my-agent", version: 1
|
|
559
|
+
|
|
527
560
|
model "gpt-4o"
|
|
528
|
-
|
|
561
|
+
|
|
562
|
+
before_llm_input ->(ctx) {
|
|
563
|
+
Phronomy::Agent::LLMInputPatch.new(
|
|
564
|
+
model_config_patch: {
|
|
565
|
+
temperature: ctx.config[:precise] ? 0.0 : 0.7
|
|
566
|
+
}
|
|
567
|
+
)
|
|
568
|
+
}
|
|
529
569
|
end
|
|
570
|
+
```
|
|
530
571
|
|
|
531
|
-
|
|
572
|
+
### Instance-level hook
|
|
573
|
+
|
|
574
|
+
```ruby
|
|
532
575
|
agent = MyAgent.new
|
|
533
|
-
agent.before_completion = ->(ctx) { { max_tokens: 512 } }
|
|
534
576
|
|
|
535
|
-
|
|
536
|
-
Phronomy.
|
|
537
|
-
|
|
577
|
+
agent.before_llm_input = ->(_ctx) {
|
|
578
|
+
Phronomy::Agent::LLMInputPatch.new(
|
|
579
|
+
model_config_patch: {
|
|
580
|
+
max_output_tokens: 512
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Global hook
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
Phronomy.configure do |config|
|
|
590
|
+
config.before_llm_input = ->(_ctx) {
|
|
591
|
+
Phronomy::Agent::LLMInputPatch.new(
|
|
592
|
+
model_config_patch: {
|
|
593
|
+
temperature: 0.3
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
}
|
|
538
597
|
end
|
|
539
598
|
```
|
|
540
599
|
|
|
541
|
-
|
|
600
|
+
When multiple hooks provide `model_config_patch`, patches are merged in hook
|
|
601
|
+
order and later values win on key conflicts.
|
|
602
|
+
|
|
603
|
+
`LLMInputPatch` can also supply `segment_candidates` for additional per-call
|
|
604
|
+
context. Those segments participate in Manifest-first input assembly. This is
|
|
605
|
+
intended for logical context supplied by the application; applications should
|
|
606
|
+
not mutate RubyLLM message history directly.
|
|
607
|
+
|
|
608
|
+
```ruby
|
|
609
|
+
Phronomy::Agent::LLMInputPatch.new(
|
|
610
|
+
segment_candidates: [
|
|
611
|
+
{
|
|
612
|
+
content: "The customer is on the enterprise plan.",
|
|
613
|
+
category: :knowledge,
|
|
614
|
+
role: :user
|
|
615
|
+
}
|
|
616
|
+
]
|
|
617
|
+
)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
The Journal remains the canonical record of observed execution history.
|
|
621
|
+
`before_llm_input` customizes the logical input assembled for a particular LLM
|
|
622
|
+
call; it does not rewrite previously recorded Journal history.
|
|
542
623
|
|
|
543
624
|
### GeneratorVerifier — Generator-Verifier loop with custom prompt builders
|
|
544
625
|
|
|
@@ -735,32 +816,78 @@ child process (stdio transport) or release the HTTP connection:
|
|
|
735
816
|
search_tool.close
|
|
736
817
|
```
|
|
737
818
|
|
|
738
|
-
###
|
|
819
|
+
### Agent State and Conversation History
|
|
820
|
+
|
|
821
|
+
Phronomy Agents are stateful objects. Each Agent has a stable `agent_id`, a persistent Agent root, an append-only execution Journal, and zero or more Agent executions.
|
|
822
|
+
|
|
823
|
+
Every concrete Agent definition must declare a stable definition identity:
|
|
824
|
+
|
|
825
|
+
```ruby
|
|
826
|
+
class ResearchAgent < Phronomy::Agent::Base
|
|
827
|
+
agent_definition id: "research-agent", version: 1
|
|
828
|
+
|
|
829
|
+
model "gpt-4o"
|
|
830
|
+
instructions "You are a research assistant."
|
|
831
|
+
end
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
The definition ID identifies the application-level Agent definition. The version is checked when a previously persisted Agent is loaded so that persisted state is not silently interpreted by an incompatible Agent definition.
|
|
739
835
|
|
|
740
|
-
|
|
741
|
-
message array and passes it in via the `messages:` keyword argument:
|
|
836
|
+
Create and continue using the same Agent instance normally:
|
|
742
837
|
|
|
743
838
|
```ruby
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
result2 = MyAgent.new.invoke(
|
|
750
|
-
"What is my name?",
|
|
751
|
-
messages: prior_messages,
|
|
752
|
-
thread_id: "session-1"
|
|
839
|
+
persistence = Phronomy::Persistence::InMemory.new
|
|
840
|
+
|
|
841
|
+
agent = ResearchAgent.create(
|
|
842
|
+
agent_id: "research-session-42",
|
|
843
|
+
persistence: persistence
|
|
753
844
|
)
|
|
754
|
-
|
|
845
|
+
|
|
846
|
+
agent.invoke("My name is Alice.")
|
|
847
|
+
result = agent.invoke("What is my name?")
|
|
848
|
+
|
|
849
|
+
puts result[:output]
|
|
755
850
|
```
|
|
756
851
|
|
|
757
|
-
`
|
|
758
|
-
|
|
852
|
+
Conversation history does not need to be passed back through `messages:` on every invocation. The Agent's canonical history is retained in its Journal and selected automatically when later LLM Calls are assembled.
|
|
853
|
+
|
|
854
|
+
A persisted Agent can be loaded again when the same Persistence backend is available:
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
agent = ResearchAgent.load(
|
|
858
|
+
"research-session-42",
|
|
859
|
+
persistence: persistence
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
result = agent.invoke("Continue our previous discussion.")
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
`result[:messages]` remains available as a materialized transcript of the Agent's current conversation history. It is a projection of canonical Agent state, not the authoritative storage mechanism and does not need to be supplied to the next `invoke`.
|
|
866
|
+
|
|
867
|
+
Existing external conversation history can be supplied when a new Agent is created:
|
|
868
|
+
|
|
869
|
+
```ruby
|
|
870
|
+
agent = ResearchAgent.create(
|
|
871
|
+
context: existing_messages,
|
|
872
|
+
persistence: persistence
|
|
873
|
+
)
|
|
874
|
+
```
|
|
759
875
|
|
|
760
|
-
|
|
761
|
-
> EventLoop session routing and compaction context. It does **not** automatically persist or
|
|
762
|
-
> restore conversation history — you must pass `messages:` explicitly on each turn as shown above.
|
|
876
|
+
Imported history must satisfy Phronomy's Import contract. User, assistant, and Tool messages are journaled without destroying their logical message boundaries. System instructions are Agent configuration and are not imported as ordinary conversation messages.
|
|
763
877
|
|
|
878
|
+
`thread_id` is an execution correlation identifier. It does not identify the persistent Agent and is not a substitute for `agent_id`.
|
|
879
|
+
|
|
880
|
+
The current conversation or memory view can be advanced without deleting the canonical Journal:
|
|
881
|
+
|
|
882
|
+
```ruby
|
|
883
|
+
agent.clear_transcript!
|
|
884
|
+
agent.clear_memory!
|
|
885
|
+
agent.reset_context!
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
These operations change which historical records belong to the active context generation. The underlying append-only Journal remains intact.
|
|
889
|
+
|
|
890
|
+
`purge!` is different: it permanently removes the Agent and its persisted execution history from the configured Persistence backend.
|
|
764
891
|
|
|
765
892
|
## Configuration
|
|
766
893
|
|
|
@@ -769,7 +896,7 @@ Phronomy.configure do |c|
|
|
|
769
896
|
c.default_model = "gpt-4o-mini"
|
|
770
897
|
c.recursion_limit = 25
|
|
771
898
|
c.tracer = Phronomy::Tracing::NullTracer.new
|
|
772
|
-
c.
|
|
899
|
+
c.before_llm_input = nil # optional global before_llm_input hook
|
|
773
900
|
c.trace_pii = false # default; set to true only when trace data contains no PII
|
|
774
901
|
c.logger = nil # optional; any object responding to #warn (e.g. Rails.logger)
|
|
775
902
|
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on shutdown
|
|
@@ -796,7 +923,7 @@ Understanding when to use each prevents scheduler stalls and hidden deadlocks.
|
|
|
796
923
|
| Context | Recommended API |
|
|
797
924
|
|---------|----------------|
|
|
798
925
|
| Top-level application code, Rails controller, background job | `agent.invoke(input)` — blocks the calling thread until done |
|
|
799
|
-
| Workflow action
|
|
926
|
+
| Workflow action | Start `invoke_async` and use `Workflow#signal` in the `on_event:` callback to deliver the result as a later Workflow event |
|
|
800
927
|
| Top-level code that wants explicit async | `agent.invoke_async(input).wait_result` — blocks the calling thread until the Task completes |
|
|
801
928
|
| Streaming from top-level code | `agent.stream(input) { |event| ... }` — blocks until done; callbacks run on the EventLoop thread |
|
|
802
929
|
| Streaming non-blocking | `task = agent.stream_async(input) { |event| ... }` — returns Task immediately; callbacks run on the EventLoop thread |
|
|
@@ -805,11 +932,21 @@ Understanding when to use each prevents scheduler stalls and hidden deadlocks.
|
|
|
805
932
|
|
|
806
933
|
### Why this matters
|
|
807
934
|
|
|
808
|
-
`invoke` is a synchronous wrapper
|
|
809
|
-
thread until the
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
935
|
+
`invoke` is a synchronous wrapper around asynchronous Agent execution and blocks
|
|
936
|
+
the calling thread until the Agent finishes. It is appropriate for top-level
|
|
937
|
+
application code such as CLI commands, controller actions, or background jobs.
|
|
938
|
+
|
|
939
|
+
Workflow entry and transition actions have a different contract: they are
|
|
940
|
+
synchronous Run-to-Completion callbacks and must finish promptly.
|
|
941
|
+
|
|
942
|
+
If a Workflow action needs an Agent or another asynchronous operation, start the
|
|
943
|
+
operation asynchronously, register its completion listener, return the Workflow
|
|
944
|
+
context, and use `Workflow#signal` to deliver completion as a later Workflow
|
|
945
|
+
event.
|
|
946
|
+
|
|
947
|
+
Do not call blocking `Agent#invoke` from an EventLoop callback, and do not return
|
|
948
|
+
the `Task` from `Agent#invoke_async` as the result of a Workflow entry or
|
|
949
|
+
transition action.
|
|
813
950
|
|
|
814
951
|
### Runtime guard
|
|
815
952
|
|
|
@@ -837,15 +974,93 @@ result = my_agent.invoke("Hello")
|
|
|
837
974
|
|
|
838
975
|
# Explicit async from top-level code
|
|
839
976
|
result = my_agent.invoke_async("Hello").wait_result
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Async work inside a Workflow
|
|
980
|
+
|
|
981
|
+
Workflow entry and transition actions are synchronous Run-to-Completion
|
|
982
|
+
callbacks.
|
|
840
983
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
984
|
+
They may start asynchronous work, but they must return the Workflow context (or
|
|
985
|
+
`nil`). Returning a `Phronomy::Task` from an entry or transition action is an
|
|
986
|
+
error.
|
|
987
|
+
|
|
988
|
+
When an asynchronous Agent finishes, deliver its result back to the live
|
|
989
|
+
Workflow as a later event with `Workflow#signal`.
|
|
990
|
+
|
|
991
|
+
```ruby
|
|
992
|
+
class AnswerContext
|
|
993
|
+
include Phronomy::WorkflowContext
|
|
994
|
+
|
|
995
|
+
field :question, type: :replace, default: ""
|
|
996
|
+
field :answer, type: :replace, default: nil
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
workflow = nil
|
|
1000
|
+
|
|
1001
|
+
workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
1002
|
+
initial :asking
|
|
1003
|
+
|
|
1004
|
+
state :asking
|
|
1005
|
+
state :done
|
|
1006
|
+
|
|
1007
|
+
entry :asking, ->(ctx) {
|
|
1008
|
+
thread_id = ctx.thread_id
|
|
1009
|
+
|
|
1010
|
+
my_agent.invoke_async(
|
|
1011
|
+
ctx.question,
|
|
1012
|
+
on_event: ->(event) {
|
|
1013
|
+
next unless event.type == :done
|
|
1014
|
+
|
|
1015
|
+
workflow.signal(
|
|
1016
|
+
thread_id: thread_id,
|
|
1017
|
+
event: :answer_ready,
|
|
1018
|
+
payload: { answer: event.payload[:output] }
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
ctx
|
|
845
1024
|
}
|
|
846
|
-
|
|
1025
|
+
|
|
1026
|
+
transition(
|
|
1027
|
+
from: :asking,
|
|
1028
|
+
on: :answer_ready,
|
|
1029
|
+
to: :done,
|
|
1030
|
+
action: ->(ctx, event) {
|
|
1031
|
+
ctx.merge(answer: event.payload[:answer])
|
|
1032
|
+
}
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
transition from: :done, to: :__finish__
|
|
1036
|
+
end
|
|
847
1037
|
```
|
|
848
1038
|
|
|
1039
|
+
The important separation is:
|
|
1040
|
+
|
|
1041
|
+
```text
|
|
1042
|
+
Workflow action
|
|
1043
|
+
│
|
|
1044
|
+
├─ starts asynchronous work
|
|
1045
|
+
│
|
|
1046
|
+
└─ returns context immediately
|
|
1047
|
+
│
|
|
1048
|
+
▼
|
|
1049
|
+
asynchronous Agent
|
|
1050
|
+
│
|
|
1051
|
+
▼
|
|
1052
|
+
on_event / callback
|
|
1053
|
+
│
|
|
1054
|
+
▼
|
|
1055
|
+
Workflow#signal
|
|
1056
|
+
│
|
|
1057
|
+
▼
|
|
1058
|
+
later Workflow event
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
`Task#map` remains a valid Task API for transforming Task results, but a mapped
|
|
1062
|
+
Task must not be returned from a Workflow entry or transition action.
|
|
1063
|
+
|
|
849
1064
|
### :immediate backend (synchronous / test mode)
|
|
850
1065
|
|
|
851
1066
|
The `:immediate` backend runs tasks synchronously using `FakeScheduler`
|
|
@@ -863,46 +1078,57 @@ end
|
|
|
863
1078
|
|
|
864
1079
|
## Context Management
|
|
865
1080
|
|
|
866
|
-
Phronomy
|
|
867
|
-
available (either from the built-in registry or via an explicit `context_window:` setting),
|
|
868
|
-
agents automatically stay within the configured token limit.
|
|
869
|
-
|
|
870
|
-
### TokenBudget
|
|
1081
|
+
Phronomy uses a Manifest-first context architecture for stateful Agents.
|
|
871
1082
|
|
|
872
|
-
|
|
1083
|
+
The main flow is:
|
|
873
1084
|
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1085
|
+
```text
|
|
1086
|
+
Canonical Journal
|
|
1087
|
+
↓
|
|
1088
|
+
Context Policy
|
|
1089
|
+
↓
|
|
1090
|
+
LLM Call Manifest
|
|
1091
|
+
↓
|
|
1092
|
+
Runtime Projection
|
|
1093
|
+
↓
|
|
1094
|
+
RubyLLM / Provider
|
|
882
1095
|
```
|
|
883
1096
|
|
|
884
|
-
|
|
1097
|
+
The **Journal** is the canonical append-only record of logical execution facts observed by Phronomy.
|
|
885
1098
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1099
|
+
The **Manifest** is the canonical logical input fixed for one particular LLM Call.
|
|
1100
|
+
|
|
1101
|
+
Context-window management therefore does not trim or rewrite the Agent's canonical history. Instead, Phronomy selects the subset of available context needed for each LLM Call and records that selection in the Manifest.
|
|
1102
|
+
|
|
1103
|
+
This distinction allows old history to remain available even when it does not fit in the current model's context window.
|
|
1104
|
+
|
|
1105
|
+
Tool protocol dependencies are preserved during selection. For example, an assistant message containing Tool Calls and the corresponding Tool-role messages are selected as a protocol-safe unit rather than independently pruning messages in a way that would create an invalid LLM conversation.
|
|
1106
|
+
|
|
1107
|
+
When the available budget is insufficient, optional historical context can be omitted from the current Manifest. Required context is never silently removed merely to satisfy the budget. If the required input cannot fit, Phronomy raises `ContextBudgetExceededError`.
|
|
1108
|
+
|
|
1109
|
+
### Context-window configuration
|
|
892
1110
|
|
|
893
|
-
|
|
1111
|
+
Phronomy derives the effective context budget from RubyLLM model metadata when available.
|
|
1112
|
+
|
|
1113
|
+
For local or otherwise unregistered models, the context window can be declared explicitly:
|
|
894
1114
|
|
|
895
1115
|
```ruby
|
|
896
|
-
class
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1116
|
+
class LocalAgent < Phronomy::Agent::Base
|
|
1117
|
+
agent_definition id: "local-agent", version: 1
|
|
1118
|
+
|
|
1119
|
+
model "local-model"
|
|
1120
|
+
context_window 32_768
|
|
1121
|
+
max_output_tokens 4_096
|
|
901
1122
|
end
|
|
902
1123
|
```
|
|
903
1124
|
|
|
904
|
-
`
|
|
905
|
-
|
|
1125
|
+
`context_window` determines the model's total context capacity.
|
|
1126
|
+
|
|
1127
|
+
`max_output_tokens` reserves capacity for the model's output.
|
|
1128
|
+
|
|
1129
|
+
The legacy `context_overhead` setting remains for compatibility with the legacy `build_context` path, but it is not the mechanism used to reserve system-prompt or Tool-definition space in Manifest-first context assembly. New Agent implementations should not rely on `context_overhead` for that purpose.
|
|
1130
|
+
|
|
1131
|
+
The current default Context Policy is framework-managed. Public custom Context Policy APIs, deterministic persistent compaction, and other advanced policy extension points are still evolving and should not yet be treated as stable application APIs.
|
|
906
1132
|
|
|
907
1133
|
> **Note on CJK languages**: The default `TokenEstimator` uses a character-ratio heuristic
|
|
908
1134
|
> calibrated for ASCII/Latin text (4 chars/token). For Chinese, Japanese, and Korean text,
|
|
@@ -922,7 +1148,7 @@ registry the budget is silently skipped.
|
|
|
922
1148
|
Pass a `CancellationToken` to any agent via `config: { cancellation_token: token }`.
|
|
923
1149
|
Cancellation is checked at multiple granular checkpoints: before the LLM call,
|
|
924
1150
|
after each streaming chunk, before each parallel
|
|
925
|
-
tool-call batch, and after each `
|
|
1151
|
+
tool-call batch, and after each `before_llm_input` hook. `CancellationError` is
|
|
926
1152
|
raised immediately. Phronomy does not replay the complete Agent invocation. No threads are force-killed — `ensure`
|
|
927
1153
|
blocks always execute.
|
|
928
1154
|
|
|
@@ -1035,7 +1261,6 @@ bundle exec ruby NN_example_name/run.rb
|
|
|
1035
1261
|
| 12 | `12_prompt_template/` | Advanced prompt templates |
|
|
1036
1262
|
| 13 | `13_mcp_http_tool/` | HTTP-based MCP tool server |
|
|
1037
1263
|
| 14 | `14_code_review/` | Automated code review agent |
|
|
1038
|
-
| 16 | `16_before_completion_hook/` | Global/class/instance before_completion hooks |
|
|
1039
1264
|
| 17 | `17_multi_agent_handoff/` | Hub-and-spoke agent routing via Runner |
|
|
1040
1265
|
|
|
1041
1266
|
The following examples are **app-level demos** (Rails apps or advanced pipelines)
|