phronomy 0.16.0 → 0.17.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 +8 -9
- data/CHANGELOG.md +54 -0
- data/CONTRIBUTING.md +28 -16
- data/README.md +124 -92
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +1 -1
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/base.rb +135 -314
- data/lib/phronomy/agent/context/capability/base.rb +166 -297
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +0 -1
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +46 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_invocation.rb +108 -314
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +15 -158
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
- data/lib/phronomy/engine/runtime.rb +15 -230
- data/lib/phronomy/engine/task_group.rb +30 -102
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
- data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +3 -120
- data/scripts/api_snapshot.rb +1 -12
- metadata +3 -9
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3af942022237dc3396d6a658f4f1b9be12f03eb0f718ebea81525822f756308
|
|
4
|
+
data.tar.gz: c046a9670fb3e1e55640f5072ac6c04399edbdfdfc4b2238caefde135fc1a2e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f52c37ff47e703de492742a2753870c4e3b4ee63ef58ab0e875afcef1488bef95f6c2f0b96e19181d1fe0f7ec28b15a9f4581f18ce0685eff28ca773eabc3aa1
|
|
7
|
+
data.tar.gz: 9cdd430036ff931dedc3548005d448f1173bfe135d7eaed8b737f01bf44e23d3467c9ef96a6a0d64d8c6005d1f48b3d57f71e39bb5c9df109fd6230ae784c64a
|
data/.mutant.yml
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
# Mutant configuration for Phronomy (opensource project)
|
|
3
|
-
# See: https://github.com/mbj/mutant
|
|
4
|
-
|
|
5
2
|
usage: opensource
|
|
6
|
-
|
|
7
3
|
integration: rspec
|
|
8
|
-
|
|
9
4
|
includes:
|
|
10
5
|
- lib
|
|
11
|
-
|
|
12
6
|
requires:
|
|
13
7
|
- phronomy
|
|
14
|
-
|
|
15
8
|
matcher:
|
|
16
9
|
subjects:
|
|
17
10
|
- Phronomy::WorkflowContext
|
|
18
11
|
- Phronomy::WorkflowRunner
|
|
19
12
|
- Phronomy::Tool::Base
|
|
20
|
-
- Phronomy::
|
|
21
|
-
- Phronomy::
|
|
13
|
+
- Phronomy::LlmContextWindow::TokenBudget
|
|
14
|
+
- Phronomy::Agent::ContextAssembler
|
|
15
|
+
- Phronomy::Agent::ContextPolicies::Default
|
|
16
|
+
- Phronomy::Agent::ContextParts::UnitBuilders::DependencyAwareUnitBuilder
|
|
17
|
+
- Phronomy::Agent::ContextParts::Requirements::RequiredContextResolver
|
|
18
|
+
- Phronomy::Agent::ContextParts::Selectors::RecentFirstSelector
|
|
19
|
+
- Phronomy::Agent::ContextParts::Budget::TokenBudgetPacker
|
|
20
|
+
- Phronomy::Agent::ContextPlanValidator
|
|
22
21
|
- Phronomy::VectorStore::InMemory
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
### Agent Context / Knowledge cleanup
|
|
13
|
+
|
|
14
|
+
#### Added
|
|
15
|
+
|
|
16
|
+
- Journal-backed persistent Agent Knowledge via creation-time `knowledge:` and
|
|
17
|
+
post-creation `Agent#add_knowledge`.
|
|
18
|
+
- `Agent#clear_knowledge!` for logical Knowledge invalidation without deleting
|
|
19
|
+
append-only Journal history.
|
|
20
|
+
- ADR-013, defining persistent Knowledge as optional Context candidates selected
|
|
21
|
+
through the Manifest-first Context Policy pipeline.
|
|
22
|
+
|
|
23
|
+
#### Changed
|
|
24
|
+
|
|
25
|
+
- Persistent Knowledge and `before_llm_input` segment candidates now participate
|
|
26
|
+
in Context Policy and token-budget selection instead of being injected as
|
|
27
|
+
mandatory system context.
|
|
28
|
+
- `clear_transcript!`, `clear_knowledge!`, and `reset_context!` now have distinct
|
|
29
|
+
transcript/Knowledge lifecycle semantics.
|
|
30
|
+
- Active Context tests, integration fixtures, benchmarks, mutation subjects,
|
|
31
|
+
design documents, and API snapshots now describe the canonical
|
|
32
|
+
Journal -> ContextCandidate -> Context Policy -> Manifest architecture.
|
|
33
|
+
|
|
34
|
+
#### Removed
|
|
35
|
+
|
|
36
|
+
- `Phronomy::KnowledgeSource`, `Knowledge::Base`, `StaticKnowledge`, and
|
|
37
|
+
`EntityKnowledge`.
|
|
38
|
+
- `static_knowledge*`, `add_knowledge_source`, `instance_knowledge_chunks`,
|
|
39
|
+
`Knowledge#static?`, `StaticKnowledge#source`, and the class-level static
|
|
40
|
+
Knowledge cache/fetch abstraction.
|
|
41
|
+
- `Agent#clear_memory!` and `AgentRoot#memory_generation`.
|
|
42
|
+
- Remaining no-op ContextVersionCache tests, fake legacy-import Context tests,
|
|
43
|
+
obsolete Memory/context integration helpers, legacy `:llm_message` benchmark
|
|
44
|
+
categories, and stale Agent `messages:` test-double signatures.
|
|
45
|
+
|
|
46
|
+
### Phase 3 cleanup
|
|
47
|
+
|
|
48
|
+
#### Changed
|
|
49
|
+
|
|
50
|
+
- Active documentation, compatibility snapshots, benchmarks, integration fixtures,
|
|
51
|
+
and mutation targets now describe the Manifest-first Context architecture.
|
|
52
|
+
- The Context benchmark now measures `Agent::ContextAssembler` and
|
|
53
|
+
`ContextPolicies::Default` instead of the removed legacy Assembler.
|
|
54
|
+
- ADR-011 is marked Superseded by ADR-012; its historical analysis is retained.
|
|
55
|
+
|
|
56
|
+
#### Removed
|
|
57
|
+
|
|
58
|
+
- Remaining active compatibility references to `context_overhead`,
|
|
59
|
+
`LlmContextWindow::Assembler`, `ContextVersionCache`, Tool
|
|
60
|
+
`on_error :return_empty`, `force_kill:`, `runtime_backend :cooperative`,
|
|
61
|
+
the `Runtime.instance=` setter, tools-splat registration, and wall-clock
|
|
62
|
+
`CancellationToken.new(deadline:)`.
|
|
63
|
+
- Obsolete legacy Assembler / ContextVersionCache executable specs.
|
|
64
|
+
|
|
65
|
+
|
|
12
66
|
---
|
|
13
67
|
|
|
14
68
|
## [0.16.0] - 2026-08-08
|
data/CONTRIBUTING.md
CHANGED
|
@@ -58,15 +58,20 @@ When adding, removing, or renaming a public method or class:
|
|
|
58
58
|
|
|
59
59
|
Key design decisions are documented as ADRs in
|
|
60
60
|
[docs/decisions/](docs/decisions/). Read these before making significant changes
|
|
61
|
-
to the threading model,
|
|
61
|
+
to the threading model, persistence/context authority, or public API shape.
|
|
62
|
+
|
|
63
|
+
For Agent Context work, ADR-012 and ADR-013 define the current Journal,
|
|
64
|
+
Manifest, Context Policy and persistent Knowledge model.
|
|
62
65
|
|
|
63
66
|
---
|
|
64
67
|
|
|
65
68
|
## Mutation Testing
|
|
66
69
|
|
|
67
|
-
Phronomy uses [mutant](https://github.com/mbj/mutant) to verify that
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
Phronomy uses [mutant](https://github.com/mbj/mutant) to verify that tests detect
|
|
71
|
+
real code changes. Mutation tests are not part of the required CI gate. The
|
|
72
|
+
repository contains `.github/workflows/nightly-mutation.yml` for mutation-run
|
|
73
|
+
automation; whether that workflow is enabled is an operational choice and is
|
|
74
|
+
not a normal pull-request requirement.
|
|
70
75
|
|
|
71
76
|
### Run mutation tests locally
|
|
72
77
|
|
|
@@ -78,18 +83,25 @@ bash scripts/run_mutation.sh
|
|
|
78
83
|
bash scripts/run_mutation.sh "Phronomy::WorkflowContext"
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
###
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
### Covered subjects
|
|
87
|
+
|
|
88
|
+
The authoritative subject list is `.mutant.yml`. It currently includes:
|
|
89
|
+
|
|
90
|
+
- `Phronomy::WorkflowContext`
|
|
91
|
+
- `Phronomy::WorkflowRunner`
|
|
92
|
+
- `Phronomy::Tool::Base`
|
|
93
|
+
- `Phronomy::LlmContextWindow::TokenBudget`
|
|
94
|
+
- `Phronomy::Agent::ContextAssembler`
|
|
95
|
+
- `Phronomy::Agent::ContextPolicies::Default`
|
|
96
|
+
- `Phronomy::Agent::ContextParts::UnitBuilders::DependencyAwareUnitBuilder`
|
|
97
|
+
- `Phronomy::Agent::ContextParts::Requirements::RequiredContextResolver`
|
|
98
|
+
- `Phronomy::Agent::ContextParts::Selectors::RecentFirstSelector`
|
|
99
|
+
- `Phronomy::Agent::ContextParts::Budget::TokenBudgetPacker`
|
|
100
|
+
- `Phronomy::Agent::ContextPlanValidator`
|
|
101
|
+
- `Phronomy::VectorStore::InMemory`
|
|
102
|
+
|
|
103
|
+
When you add or modify tests for a covered subject, run mutation tests locally
|
|
104
|
+
when practical and investigate meaningful score regressions.
|
|
93
105
|
|
|
94
106
|
---
|
|
95
107
|
|
data/README.md
CHANGED
|
@@ -42,7 +42,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
42
42
|
|
|
43
43
|
| Feature | Stability |
|
|
44
44
|
|---|---|
|
|
45
|
-
| **Knowledge** —
|
|
45
|
+
| **Knowledge** — Journal-backed persistent Agent context registered with `knowledge:` / `add_knowledge`; selected per LLM call by Context Policy; `clear_knowledge!` logically resets retained Knowledge without deleting Journal history | Beta |
|
|
46
46
|
| **`VectorStore#size`** — Returns document count for all three backends (InMemory, RedisSearch, Pgvector) | Beta |
|
|
47
47
|
| **`VectorStore::AsyncBackend` mixin** — Pluggable async interface for `VectorStore`; default pool-backed implementations for `search_async`, `add_async`, `remove_async`, `clear_async`; backends with native async drivers override individual methods to bypass `BlockingAdapterPool` entirely; all existing backends remain unchanged | Beta |
|
|
48
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 |
|
|
@@ -57,8 +57,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Filters, an
|
|
|
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
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
|
-
| **CancellationToken** — Cooperative cancellation via `cancel!`/`cancelled?`/`raise_if_cancelled!`; `timeout_after(seconds)` for monotonic-clock deadlines;
|
|
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 |
|
|
60
|
+
| **CancellationToken** — Cooperative cancellation via `cancel!`/`cancelled?`/`raise_if_cancelled!`; `timeout_after(seconds)` for monotonic-clock deadlines; 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 |
|
|
62
61
|
| **`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
62
|
| **`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
63
|
| **`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 |
|
|
@@ -99,7 +98,7 @@ The APIs listed below are intended for advanced use cases, framework internals,
|
|
|
99
98
|
|---|---|
|
|
100
99
|
| **`Phronomy::Diagnostics`** — Snapshot of scheduler internals for debug/monitoring; `SchedulerReentrancyError` raised on invalid re-entrant scheduler use; `Runtime.in_scheduler_context?` returns `true` when called from inside a scheduler task | Experimental |
|
|
101
100
|
| **`Phronomy::Testing::FakeClock` / `FakeScheduler` / `SchedulerHelpers`** — Test helpers for deterministic concurrency specs: `FakeClock#advance(seconds)` controls time; `FakeScheduler` runs tasks synchronously and records `event_log`; `FakeScheduler#assert_order` / `#assert_cancelled` for ordering assertions; `FakeClock#advance_to_next_timer` fires the next pending callback; `Testing::SchedulerHelpers#with_fake_scheduler` replaces the global Runtime for the duration of a block | Beta |
|
|
102
|
-
| **`Configuration#runtime_backend`** — `:thread` (default, one OS thread per task), `:immediate` (tests — tasks run synchronously, no extra threads), `:fiber` (**EXPERIMENTAL** —
|
|
101
|
+
| **`Configuration#runtime_backend`** — `:thread` (default, one OS thread per task), `:immediate` (tests — tasks run synchronously, no extra threads), `:fiber` (**EXPERIMENTAL** — validation backend only: runs tasks as Ruby Fibers on a cooperative scheduler to verify that framework components are truly non-blocking; **not for production use** and not a planned production replacement for `:thread`; no preemptive scheduling will be added) | Beta |
|
|
103
102
|
| **`Configuration#strict_runtime_guards`** — When `true`, calling `Agent#invoke` from inside a scheduler task raises `SchedulerReentrancyError`; when `false` (default) a warning is logged instead | Beta |
|
|
104
103
|
|
|
105
104
|
## Installation
|
|
@@ -150,6 +149,28 @@ See the [RubyLLM documentation](https://rubyllm.com) for all supported providers
|
|
|
150
149
|
| `InvocationContext#provider_limits` | Configure the provider client directly |
|
|
151
150
|
| `stream_queue_max_size` | No replacement; the shared EventLoop queue is unbounded by design. Monitor `Metrics.snapshot[:event_loop_queue_depth]` instead |
|
|
152
151
|
|
|
152
|
+
### 0.16 cleanup migration
|
|
153
|
+
|
|
154
|
+
The following compatibility-only APIs have been removed from the active contract:
|
|
155
|
+
|
|
156
|
+
| Removed API | Current contract |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `context_overhead` | Manifest-first assembly budgets actual mandatory + selected content |
|
|
159
|
+
| Tool `on_error :return_empty` | Use `:raise` or `:suppress` |
|
|
160
|
+
| `dispatch_parallel(..., force_kill:)` / `fan_out(..., force_kill:)` | Cooperative cancellation; no force-kill switch |
|
|
161
|
+
| `runtime_backend = :cooperative` | Use `:thread`, `:immediate`, or experimental `:fiber` explicitly |
|
|
162
|
+
| `Runtime.instance = ...` | Runtime replacement is test/internal infrastructure, not a public setter |
|
|
163
|
+
| `TaskGroup.new(runtime: nil)` | Runtime is required |
|
|
164
|
+
| `tools ToolA, ToolB` | Use `tools(ToolA => nil, ToolB => nil)` |
|
|
165
|
+
| `CancellationToken.new(deadline: Time...)` | Use `CancellationToken.timeout_after(seconds)` for token deadlines |
|
|
166
|
+
| `StaticKnowledge` / `EntityKnowledge` / `Knowledge::Base` / `Phronomy::KnowledgeSource` | Register plain persistent Knowledge with `knowledge:` or `add_knowledge` |
|
|
167
|
+
| `static_knowledge*` class APIs | Persistent Knowledge belongs to Agent instances and is Journal-backed |
|
|
168
|
+
| `clear_memory!` | Use `clear_knowledge!`; conversation history is controlled independently with `clear_transcript!` |
|
|
169
|
+
|
|
170
|
+
The legacy `build_context` / `LlmContextWindow::Assembler` extension path is no
|
|
171
|
+
longer an active API. Stateful Agent input is assembled through the canonical
|
|
172
|
+
Journal → Context Policy → LLM Input Manifest pipeline.
|
|
173
|
+
|
|
153
174
|
### Optional dependencies
|
|
154
175
|
|
|
155
176
|
Install additional gems only for the features you use:
|
|
@@ -179,7 +200,7 @@ class ResearchAgent < Phronomy::Agent::Base
|
|
|
179
200
|
agent_definition id: "research-agent", version: 1
|
|
180
201
|
model "gpt-4o"
|
|
181
202
|
instructions "You are a research assistant. Use tools to answer questions."
|
|
182
|
-
tools
|
|
203
|
+
tools(WebSearch => nil)
|
|
183
204
|
max_iterations 5
|
|
184
205
|
end
|
|
185
206
|
|
|
@@ -434,7 +455,7 @@ end
|
|
|
434
455
|
class OrchestratorAgent < Phronomy::Agent::Base
|
|
435
456
|
model "gpt-4o"
|
|
436
457
|
instructions "Use the research tool first, then the write tool to produce a blog post."
|
|
437
|
-
tools
|
|
458
|
+
tools(ResearchTool => nil, WriteTool => nil)
|
|
438
459
|
end
|
|
439
460
|
|
|
440
461
|
result = OrchestratorAgent.new.invoke("Write a blog post about Ruby 3.4 features")
|
|
@@ -470,39 +491,56 @@ end
|
|
|
470
491
|
> that logic must be implemented by the application. Reference implementations for
|
|
471
492
|
> common patterns are available in `phronomy-examples` (example 06).
|
|
472
493
|
|
|
473
|
-
### Knowledge —
|
|
494
|
+
### Knowledge — Persistent Agent context
|
|
495
|
+
|
|
496
|
+
Knowledge is plain logical content retained by one Agent and considered by
|
|
497
|
+
Context Policy for future LLM calls. It is not a separate source-object type and
|
|
498
|
+
it is not automatically mandatory.
|
|
499
|
+
|
|
500
|
+
Register initial Knowledge when creating the Agent:
|
|
474
501
|
|
|
475
502
|
```ruby
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
503
|
+
policy_text = File.read("policy.md")
|
|
504
|
+
|
|
505
|
+
agent = ResearchAgent.new(
|
|
506
|
+
knowledge: [
|
|
507
|
+
policy_text,
|
|
508
|
+
"Customer tier: enterprise"
|
|
509
|
+
]
|
|
481
510
|
)
|
|
511
|
+
```
|
|
482
512
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
513
|
+
Add durable Knowledge later:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
agent.add_knowledge(
|
|
517
|
+
"Customer locale: ja-JP",
|
|
518
|
+
metadata: {"origin" => "customer_profile"}
|
|
519
|
+
)
|
|
488
520
|
```
|
|
489
521
|
|
|
490
|
-
|
|
491
|
-
|
|
522
|
+
Persistent Knowledge is Journal-backed, survives `Agent.load`, and is excluded
|
|
523
|
+
from the public conversation transcript. `clear_knowledge!` logically
|
|
524
|
+
invalidates earlier Knowledge while keeping the append-only Journal intact.
|
|
492
525
|
|
|
493
526
|
```ruby
|
|
494
|
-
|
|
495
|
-
# Call refresh! when the underlying content changes (e.g. after reloading policy.md).
|
|
496
|
-
MyAgent.static_knowledge_refresh!
|
|
527
|
+
agent.clear_knowledge!
|
|
497
528
|
```
|
|
498
529
|
|
|
499
|
-
|
|
530
|
+
For request-scoped retrieval results that should not be persisted, use
|
|
531
|
+
`before_llm_input` `segment_candidates` instead.
|
|
532
|
+
|
|
533
|
+
Load and split documents with built-in loaders when the application needs an
|
|
534
|
+
acquisition/RAG pipeline:
|
|
500
535
|
|
|
501
536
|
```ruby
|
|
502
537
|
chunks = Phronomy::VectorStore::Loader::MarkdownLoader.new.load("docs/guide.md")
|
|
503
538
|
.then { |docs| Phronomy::VectorStore::Splitter::RecursiveSplitter.new(chunk_size: 512).split(docs) }
|
|
504
539
|
```
|
|
505
540
|
|
|
541
|
+
The application decides whether retrieved/extracted information becomes durable
|
|
542
|
+
Agent Knowledge (`add_knowledge`) or per-call Context (`before_llm_input`).
|
|
543
|
+
|
|
506
544
|
### Multi-Agent Handoff — Hub-and-spoke routing
|
|
507
545
|
|
|
508
546
|
```ruby
|
|
@@ -601,9 +639,9 @@ When multiple hooks provide `model_config_patch`, patches are merged in hook
|
|
|
601
639
|
order and later values win on key conflicts.
|
|
602
640
|
|
|
603
641
|
`LLMInputPatch` can also supply `segment_candidates` for additional per-call
|
|
604
|
-
context. Those
|
|
605
|
-
|
|
606
|
-
not
|
|
642
|
+
context. Those candidates enter the same Context Policy selection path as
|
|
643
|
+
persistent/history candidates and are not automatically mandatory. They are
|
|
644
|
+
not persisted to the Journal.
|
|
607
645
|
|
|
608
646
|
```ruby
|
|
609
647
|
Phronomy::Agent::LLMInputPatch.new(
|
|
@@ -617,9 +655,9 @@ Phronomy::Agent::LLMInputPatch.new(
|
|
|
617
655
|
)
|
|
618
656
|
```
|
|
619
657
|
|
|
620
|
-
The Journal remains the canonical record of observed execution history
|
|
621
|
-
`before_llm_input` customizes the logical
|
|
622
|
-
|
|
658
|
+
The Journal remains the canonical record of observed execution history and
|
|
659
|
+
persistent Knowledge. `before_llm_input` customizes only the logical candidate
|
|
660
|
+
set for a particular LLM call.
|
|
623
661
|
|
|
624
662
|
### GeneratorVerifier — Generator-Verifier loop with custom prompt builders
|
|
625
663
|
|
|
@@ -671,8 +709,7 @@ end
|
|
|
671
709
|
|
|
672
710
|
### MultiAgent::Orchestrator — Parallel subagent dispatch
|
|
673
711
|
|
|
674
|
-
> **Note:** `
|
|
675
|
-
> `max_concurrency:` to cap the number of concurrent workers and `on_error:`
|
|
712
|
+
> **Note:** Use `max_concurrency:` to cap concurrent workers and `on_error:`
|
|
676
713
|
> to control failure handling (`:raise` re-raises the first error after all
|
|
677
714
|
> tasks complete; `:skip` fills failed slots with `nil`). For very large
|
|
678
715
|
> fan-outs consider additional rate-limiting at the application level.
|
|
@@ -698,7 +735,6 @@ class MyOrchestrator < Phronomy::MultiAgent::Orchestrator
|
|
|
698
735
|
instructions "Orchestrate."
|
|
699
736
|
|
|
700
737
|
def run(query)
|
|
701
|
-
# Heterogeneous agents in parallel (cap at 4 threads; skip failures; 30 s timeout)
|
|
702
738
|
results = dispatch_parallel(
|
|
703
739
|
{agent: SearchAgent, input: "topic A"},
|
|
704
740
|
{agent: AnalysisAgent, input: query},
|
|
@@ -707,7 +743,6 @@ class MyOrchestrator < Phronomy::MultiAgent::Orchestrator
|
|
|
707
743
|
timeout: 30
|
|
708
744
|
)
|
|
709
745
|
|
|
710
|
-
# Fan-out — same agent, multiple inputs
|
|
711
746
|
translations = fan_out(
|
|
712
747
|
agent: TranslationAgent,
|
|
713
748
|
inputs: %w[Hello World],
|
|
@@ -736,14 +771,10 @@ end
|
|
|
736
771
|
app = Phronomy::Workflow.define(EnrichContext) do
|
|
737
772
|
initial :enrich
|
|
738
773
|
state :enrich, action: ->(s) do
|
|
739
|
-
# Use Thread#value to collect results safely — avoids concurrent Hash writes
|
|
740
774
|
threads = {
|
|
741
775
|
summary: Thread.new { Summarizer.call(s) },
|
|
742
776
|
tags: Thread.new { Tagger.call(s) }
|
|
743
777
|
}
|
|
744
|
-
# For bounded waits, use Thread#join(timeout_seconds); nil means timed out — handle explicitly.
|
|
745
|
-
# Do not use Timeout.timeout or Thread#kill — both inject async exceptions that bypass cleanup.
|
|
746
|
-
# Prefer CancellationToken for cooperative cancellation of Phronomy-managed tasks.
|
|
747
778
|
threads.each_value(&:join)
|
|
748
779
|
s.merge(summary: threads[:summary].value, tags: Array(threads[:tags].value))
|
|
749
780
|
end
|
|
@@ -756,12 +787,10 @@ state = app.invoke({}, config: { thread_id: "t1" })
|
|
|
756
787
|
### Output Parser — Structured LLM responses
|
|
757
788
|
|
|
758
789
|
```ruby
|
|
759
|
-
# Extract JSON from LLM output (handles Markdown code fences automatically)
|
|
760
790
|
parser = Phronomy::OutputParser::JsonParser.new
|
|
761
791
|
data = parser.parse('```json\n{"name":"Alice","score":0.9}\n```')
|
|
762
792
|
# => { name: "Alice", score: 0.9 }
|
|
763
793
|
|
|
764
|
-
# Map JSON directly to a Struct
|
|
765
794
|
PersonSchema = Struct.new(:name, :age, keyword_init: true)
|
|
766
795
|
parser = Phronomy::OutputParser::StructuredParser.new(PersonSchema)
|
|
767
796
|
person = parser.parse('{"name":"Alice","age":30}')
|
|
@@ -784,15 +813,15 @@ runner = Phronomy::Eval::Runner.new(
|
|
|
784
813
|
results = runner.run(dataset, ->(q) { agent.invoke(q) })
|
|
785
814
|
metrics = Phronomy::Eval::Metrics.new(results)
|
|
786
815
|
|
|
787
|
-
puts "Mean score: #{metrics.mean_score}"
|
|
788
|
-
puts "Pass rate: #{metrics.pass_rate}"
|
|
816
|
+
puts "Mean score: #{metrics.mean_score}"
|
|
817
|
+
puts "Pass rate: #{metrics.pass_rate}"
|
|
789
818
|
```
|
|
790
819
|
|
|
791
820
|
### Tracing — Custom observability
|
|
792
821
|
|
|
793
822
|
```ruby
|
|
794
823
|
Phronomy.configure do |c|
|
|
795
|
-
c.tracer = MyCustomTracer.new
|
|
824
|
+
c.tracer = MyCustomTracer.new
|
|
796
825
|
end
|
|
797
826
|
```
|
|
798
827
|
|
|
@@ -840,10 +869,12 @@ persistence = Phronomy::Persistence::InMemory.new
|
|
|
840
869
|
|
|
841
870
|
agent = ResearchAgent.create(
|
|
842
871
|
agent_id: "research-session-42",
|
|
872
|
+
knowledge: ["Customer tier: enterprise"],
|
|
843
873
|
persistence: persistence
|
|
844
874
|
)
|
|
845
875
|
|
|
846
876
|
agent.invoke("My name is Alice.")
|
|
877
|
+
agent.add_knowledge("Customer locale: ja-JP")
|
|
847
878
|
result = agent.invoke("What is my name?")
|
|
848
879
|
|
|
849
880
|
puts result[:output]
|
|
@@ -869,6 +900,7 @@ Existing external conversation history can be supplied when a new Agent is creat
|
|
|
869
900
|
```ruby
|
|
870
901
|
agent = ResearchAgent.create(
|
|
871
902
|
context: existing_messages,
|
|
903
|
+
knowledge: initial_knowledge,
|
|
872
904
|
persistence: persistence
|
|
873
905
|
)
|
|
874
906
|
```
|
|
@@ -877,16 +909,14 @@ Imported history must satisfy Phronomy's Import contract. User, assistant, and T
|
|
|
877
909
|
|
|
878
910
|
`thread_id` is an execution correlation identifier. It does not identify the persistent Agent and is not a substitute for `agent_id`.
|
|
879
911
|
|
|
880
|
-
The
|
|
912
|
+
The active conversation and Knowledge views can be advanced independently without deleting the canonical Journal:
|
|
881
913
|
|
|
882
914
|
```ruby
|
|
883
|
-
agent.clear_transcript!
|
|
884
|
-
agent.
|
|
885
|
-
agent.reset_context!
|
|
915
|
+
agent.clear_transcript! # conversation only
|
|
916
|
+
agent.clear_knowledge! # persistent Knowledge only
|
|
917
|
+
agent.reset_context! # both
|
|
886
918
|
```
|
|
887
919
|
|
|
888
|
-
These operations change which historical records belong to the active context generation. The underlying append-only Journal remains intact.
|
|
889
|
-
|
|
890
920
|
`purge!` is different: it permanently removes the Agent and its persisted execution history from the configured Persistence backend.
|
|
891
921
|
|
|
892
922
|
## Configuration
|
|
@@ -896,11 +926,11 @@ Phronomy.configure do |c|
|
|
|
896
926
|
c.default_model = "gpt-4o-mini"
|
|
897
927
|
c.recursion_limit = 25
|
|
898
928
|
c.tracer = Phronomy::Tracing::NullTracer.new
|
|
899
|
-
c.before_llm_input
|
|
929
|
+
c.before_llm_input = nil # optional global before_llm_input hook
|
|
900
930
|
c.trace_pii = false # default; set to true only when trace data contains no PII
|
|
901
931
|
c.logger = nil # optional; any object responding to #warn (e.g. Rails.logger)
|
|
902
932
|
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on shutdown
|
|
903
|
-
c.runtime_backend = :thread # :thread (default); :immediate (tests, synchronous); :fiber (experimental validation only)
|
|
933
|
+
c.runtime_backend = :thread # :thread (default); :immediate (tests, synchronous); :fiber (experimental validation only)
|
|
904
934
|
c.strict_runtime_guards = false # when true, raises SchedulerReentrancyError on invoke-inside-task
|
|
905
935
|
c.stream_callback_error_policy = :report # :report (default) preserves Agent result; :fail_task fails Task with StreamCallbackError
|
|
906
936
|
end
|
|
@@ -953,26 +983,20 @@ transition action.
|
|
|
953
983
|
Phronomy detects this pattern automatically:
|
|
954
984
|
|
|
955
985
|
```ruby
|
|
956
|
-
# Default (soft mode): logs a warning and continues
|
|
957
986
|
Phronomy.configure { |c| c.strict_runtime_guards = false }
|
|
958
|
-
|
|
959
|
-
# Strict mode: raises SchedulerReentrancyError immediately
|
|
960
987
|
Phronomy.configure { |c| c.strict_runtime_guards = true }
|
|
961
988
|
```
|
|
962
989
|
|
|
963
990
|
You can also query the current context directly:
|
|
964
991
|
|
|
965
992
|
```ruby
|
|
966
|
-
Phronomy::Runtime.in_scheduler_context?
|
|
993
|
+
Phronomy::Runtime.in_scheduler_context?
|
|
967
994
|
```
|
|
968
995
|
|
|
969
996
|
### Migration: blocking wait → Task mapping
|
|
970
997
|
|
|
971
998
|
```ruby
|
|
972
|
-
# Top-level synchronous use
|
|
973
999
|
result = my_agent.invoke("Hello")
|
|
974
|
-
|
|
975
|
-
# Explicit async from top-level code
|
|
976
1000
|
result = my_agent.invoke_async("Hello").wait_result
|
|
977
1001
|
```
|
|
978
1002
|
|
|
@@ -1064,7 +1088,7 @@ Task must not be returned from a Workflow entry or transition action.
|
|
|
1064
1088
|
### :immediate backend (synchronous / test mode)
|
|
1065
1089
|
|
|
1066
1090
|
The `:immediate` backend runs tasks synchronously using `FakeScheduler`
|
|
1067
|
-
(backed by `Task::ImmediateBackend`).
|
|
1091
|
+
(backed by `Task::ImmediateBackend`). Blocking I/O is isolated in `BlockingAdapterPool`.
|
|
1068
1092
|
To switch back to the default thread-per-task backend:
|
|
1069
1093
|
|
|
1070
1094
|
```ruby
|
|
@@ -1080,11 +1104,11 @@ end
|
|
|
1080
1104
|
|
|
1081
1105
|
Phronomy uses a Manifest-first context architecture for stateful Agents.
|
|
1082
1106
|
|
|
1083
|
-
The main flow is:
|
|
1084
|
-
|
|
1085
1107
|
```text
|
|
1086
1108
|
Canonical Journal
|
|
1087
1109
|
↓
|
|
1110
|
+
Context candidates
|
|
1111
|
+
↓
|
|
1088
1112
|
Context Policy
|
|
1089
1113
|
↓
|
|
1090
1114
|
LLM Call Manifest
|
|
@@ -1094,17 +1118,34 @@ Runtime Projection
|
|
|
1094
1118
|
RubyLLM / Provider
|
|
1095
1119
|
```
|
|
1096
1120
|
|
|
1097
|
-
The **Journal** is the canonical append-only record of logical execution facts
|
|
1121
|
+
The **Journal** is the canonical append-only record of logical execution facts
|
|
1122
|
+
observed by Phronomy and persistent Knowledge explicitly registered by the
|
|
1123
|
+
application.
|
|
1098
1124
|
|
|
1099
|
-
The **Manifest** is the canonical logical input fixed for one particular LLM
|
|
1125
|
+
The **Manifest** is the canonical logical input fixed for one particular LLM
|
|
1126
|
+
Call.
|
|
1100
1127
|
|
|
1101
|
-
Context-window management therefore does not trim or rewrite the Agent's
|
|
1128
|
+
Context-window management therefore does not trim or rewrite the Agent's
|
|
1129
|
+
canonical history. Phronomy selects the subset of available optional Context
|
|
1130
|
+
needed for each LLM Call and records that selection in the Manifest.
|
|
1102
1131
|
|
|
1103
|
-
|
|
1132
|
+
Persistent Knowledge is an ordinary `:knowledge` Context candidate. It is not
|
|
1133
|
+
concatenated into the mandatory system prompt. Conversation history and
|
|
1134
|
+
Knowledge share the same policy/budget selection path while remaining distinct
|
|
1135
|
+
in public transcript semantics.
|
|
1104
1136
|
|
|
1105
|
-
|
|
1137
|
+
Per-call `before_llm_input` segment candidates also pass through Context Policy
|
|
1138
|
+
and are not written to the Journal.
|
|
1106
1139
|
|
|
1107
|
-
|
|
1140
|
+
Tool protocol dependencies are preserved during selection. An assistant message
|
|
1141
|
+
containing Tool Calls and the corresponding Tool-role messages are selected as
|
|
1142
|
+
a protocol-safe unit rather than independently pruning messages in a way that
|
|
1143
|
+
would create an invalid LLM conversation.
|
|
1144
|
+
|
|
1145
|
+
When the available budget is insufficient, optional history or Knowledge can be
|
|
1146
|
+
omitted from the current Manifest. Required context is never silently removed
|
|
1147
|
+
merely to satisfy the budget. If required input cannot fit, Phronomy raises
|
|
1148
|
+
`ContextBudgetExceededError`.
|
|
1108
1149
|
|
|
1109
1150
|
### Context-window configuration
|
|
1110
1151
|
|
|
@@ -1126,9 +1167,14 @@ end
|
|
|
1126
1167
|
|
|
1127
1168
|
`max_output_tokens` reserves capacity for the model's output.
|
|
1128
1169
|
|
|
1129
|
-
|
|
1170
|
+
Mandatory instructions, current input and Tool definitions are budgeted from
|
|
1171
|
+
their actual canonical values. `context_overhead` is not part of the current
|
|
1172
|
+
contract.
|
|
1130
1173
|
|
|
1131
|
-
The current default Context Policy is framework-managed. Public custom Context
|
|
1174
|
+
The current default Context Policy is framework-managed. Public custom Context
|
|
1175
|
+
Policy APIs, deterministic persistent compaction, and other advanced policy
|
|
1176
|
+
extension points are still evolving and should not yet be treated as stable
|
|
1177
|
+
application APIs.
|
|
1132
1178
|
|
|
1133
1179
|
> **Note on CJK languages**: The default `TokenEstimator` uses a character-ratio heuristic
|
|
1134
1180
|
> calibrated for ASCII/Latin text (4 chars/token). For Chinese, Japanese, and Korean text,
|
|
@@ -1142,15 +1188,14 @@ The current default Context Policy is framework-managed. Public custom Context P
|
|
|
1142
1188
|
> Phronomy::LlmContextWindow::TokenEstimator.tokenizer = ->(text) { enc.encode(text).length }
|
|
1143
1189
|
> ```
|
|
1144
1190
|
|
|
1145
|
-
|
|
1146
1191
|
### CancellationToken — Cooperative cancellation
|
|
1147
1192
|
|
|
1148
1193
|
Pass a `CancellationToken` to any agent via `config: { cancellation_token: token }`.
|
|
1149
1194
|
Cancellation is checked at multiple granular checkpoints: before the LLM call,
|
|
1150
|
-
after each streaming chunk, before each parallel
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
blocks always execute.
|
|
1195
|
+
after each streaming chunk, before each parallel tool-call batch, and after each
|
|
1196
|
+
`before_llm_input` hook. `CancellationError` is raised immediately. Phronomy
|
|
1197
|
+
does not replay the complete Agent invocation. No threads are force-killed —
|
|
1198
|
+
`ensure` blocks always execute.
|
|
1154
1199
|
|
|
1155
1200
|
> **Cooperative cancellation — not preemptive**
|
|
1156
1201
|
>
|
|
@@ -1158,19 +1203,14 @@ blocks always execute.
|
|
|
1158
1203
|
> checkpoints listed above; it is **not** injected as a signal into a running
|
|
1159
1204
|
> operation. This means the following are **not** interrupted mid-execution:
|
|
1160
1205
|
>
|
|
1161
|
-
> -
|
|
1206
|
+
> - An application retrieval/load operation that is already blocking
|
|
1162
1207
|
> - A single `chat.ask` call that is not streaming
|
|
1163
1208
|
> - A single `tool.execute` call that is already running
|
|
1164
1209
|
> - Any external I/O (database query, vector search, HTTP request) inside those calls
|
|
1165
1210
|
>
|
|
1166
|
-
> For deep in-flight safety, complement `CancellationToken` with
|
|
1167
|
-
>
|
|
1168
|
-
> database `statement_timeout`, or Redis client timeout
|
|
1169
|
-
> to abort cleanly. Avoid `Timeout.timeout` unless you understand its async-exception
|
|
1170
|
-
> risks: it injects `Timeout::Error` at an arbitrary execution point (the same
|
|
1171
|
-
> mechanism as `Thread#kill`), which Phronomy avoids by default due to resource
|
|
1172
|
-
> safety concerns. Ruby's GVL prevents fully preemptive cancellation without such
|
|
1173
|
-
> risky interruption.
|
|
1211
|
+
> For deep in-flight safety, complement `CancellationToken` with operation-native
|
|
1212
|
+
> timeouts. Prefer library-native timeouts such as `Net::HTTP#read_timeout`,
|
|
1213
|
+
> database `statement_timeout`, or Redis client timeout.
|
|
1174
1214
|
|
|
1175
1215
|
> **`timeout_after` vs `CancellationScope.deadline_in`**
|
|
1176
1216
|
>
|
|
@@ -1196,18 +1236,16 @@ blocks always execute.
|
|
|
1196
1236
|
> Phronomy does not interpret `config[:llm_timeout]`, `config[:tool_timeout]`,
|
|
1197
1237
|
> Agent `retry_policy`, or Tool `retry_on`. Configure LLM transport behavior on
|
|
1198
1238
|
> RubyLLM (or another adapter) and configure Tool transport behavior on the Tool's
|
|
1199
|
-
> HTTP/DB/MCP client.
|
|
1200
|
-
> the timeout and retry semantics.
|
|
1239
|
+
> HTTP/DB/MCP client.
|
|
1201
1240
|
>
|
|
1202
1241
|
> `InvocationContext#deadline` and `cancellation_token` remain available for a
|
|
1203
1242
|
> caller-defined root-operation boundary. They provide cooperative cancellation
|
|
1204
1243
|
> across the Phronomy execution tree; they do not replace provider-native socket,
|
|
1205
1244
|
> request, statement, or session timeouts.
|
|
1206
|
-
|
|
1245
|
+
|
|
1207
1246
|
```ruby
|
|
1208
1247
|
token = Phronomy::Concurrency::CancellationToken.new
|
|
1209
1248
|
|
|
1210
|
-
# Cancel from another thread after 5 s
|
|
1211
1249
|
Thread.new { sleep 5; token.cancel! }
|
|
1212
1250
|
|
|
1213
1251
|
begin
|
|
@@ -1216,15 +1254,9 @@ rescue Phronomy::CancellationError
|
|
|
1216
1254
|
puts "cancelled"
|
|
1217
1255
|
end
|
|
1218
1256
|
|
|
1219
|
-
# Hard deadline via monotonic clock (recommended — immune to NTP/DST changes)
|
|
1220
1257
|
token = Phronomy::Concurrency::CancellationToken.timeout_after(30)
|
|
1221
1258
|
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
1222
1259
|
|
|
1223
|
-
# Hard deadline via wall-clock (legacy — still supported)
|
|
1224
|
-
token = Phronomy::Concurrency::CancellationToken.new(deadline: Time.now + 30)
|
|
1225
|
-
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
1226
|
-
|
|
1227
|
-
# Propagate to all parallel workers via dispatch_parallel / fan_out
|
|
1228
1260
|
token = Phronomy::Concurrency::CancellationToken.new
|
|
1229
1261
|
Thread.new { sleep 10; token.cancel! }
|
|
1230
1262
|
|