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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +54 -0
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +124 -92
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +1 -1
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  12. data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
  13. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  14. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  15. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
  17. data/lib/phronomy/agent/agent_root.rb +1 -2
  18. data/lib/phronomy/agent/base.rb +135 -314
  19. data/lib/phronomy/agent/context/capability/base.rb +166 -297
  20. data/lib/phronomy/agent/context_assembler.rb +65 -29
  21. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  22. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  23. data/lib/phronomy/agent/execution_coordinator.rb +0 -1
  24. data/lib/phronomy/agent/journal_projection.rb +28 -2
  25. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  26. data/lib/phronomy/agent/shared_state.rb +46 -138
  27. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  28. data/lib/phronomy/agent/tool_invocation.rb +108 -314
  29. data/lib/phronomy/agent.rb +6 -10
  30. data/lib/phronomy/configuration.rb +15 -158
  31. data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
  32. data/lib/phronomy/engine/runtime.rb +15 -230
  33. data/lib/phronomy/engine/task_group.rb +30 -102
  34. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  35. data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
  36. data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
  37. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  38. data/lib/phronomy/version.rb +1 -1
  39. data/lib/phronomy.rb +3 -120
  40. data/scripts/api_snapshot.rb +1 -12
  41. metadata +3 -9
  42. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  43. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  44. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  45. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  46. data/lib/phronomy/knowledge_source.rb +0 -12
  47. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  48. 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: 633f62428045fed9447fd95414687cf8f2d0df11163afd767a73cc1c9b0ea97e
4
- data.tar.gz: 68d352c78047e7b87dade2f8659cf44d4ecd16acf506090329d3e276f8a35f0c
3
+ metadata.gz: d3af942022237dc3396d6a658f4f1b9be12f03eb0f718ebea81525822f756308
4
+ data.tar.gz: c046a9670fb3e1e55640f5072ac6c04399edbdfdfc4b2238caefde135fc1a2e5
5
5
  SHA512:
6
- metadata.gz: 67cd757c8efacd34360a3b876bae0ebbac0e37080f77472a9b69eaf774f14b5f2c070288d4e5d66bbfcc31a6be60353ca043e4f95ee6b1a14da09e8fe5792b00
7
- data.tar.gz: '0382e7bbf708605dea900725e7ab4c093baf653ef0e88c11ba79c6ab227b1584624aa73b9b16cacd8644d4d1aafe0ffade1b6b1d6361a1f33d7395e730b4e1df'
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::Context::TokenBudget
21
- - Phronomy::Context::TokenEstimator
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, caching strategy, or public API shape.
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 each test
68
- actually detects real code changes. Mutation tests are **not** part of the
69
- required CI gate (they are slow), but run nightly via `.github/workflows/nightly-mutation.yml`.
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
- ### Coverage targets
82
-
83
- | Subject | Baseline | Target |
84
- |---|---|---|
85
- | `Phronomy::WorkflowContext` | 84.85% | ≥ 80% |
86
- | `Phronomy::WorkflowRunner` | — | ≥ 80% |
87
- | `Phronomy::Tool::Base` | 55.74% | ≥ 80% |
88
- | `Phronomy::Context::TokenBudget` | — | ≥ 80% |
89
- | `Phronomy::VectorStore::InMemory` | — | ≥ 80% |
90
-
91
- When you add or modify tests for a covered subject, run mutation tests to confirm
92
- the score does not regress.
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** — Static context injection with pluggable loaders, splitters, and vector stores; `static_knowledge_refresh!` for runtime cache invalidation | Beta |
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; 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
- | **`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** — 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). `:cooperative` is a **deprecated alias** for `:immediate` — do not use in new code | Beta |
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 WebSearch
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 ResearchTool, WriteTool
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 — Static context injection
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
- # Static knowledge (policy files, reference docs)
477
- policy = Phronomy::Agent::Context::Knowledge::StaticKnowledge.new(
478
- File.read("policy.md"),
479
- type: :policy,
480
- source: "policy.md" # exposed to LLM for citation
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
- # Inject at invocation time via the agent DSL
484
- class MyAgent < Phronomy::Agent::Base
485
- model "gpt-4o-mini"
486
- knowledge policy
487
- end
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
- `static_knowledge_refresh!` invalidates the class-level cache of static knowledge sources.
491
- Call it when the underlying file or content has changed:
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
- # Static knowledge sources are cached at the class level after the first fetch.
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
- Load and split documents with built-in loaders:
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 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.
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 input assembled for a particular LLM
622
- call; it does not rewrite previously recorded Journal history.
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:** `dispatch_parallel` and `fan_out` use plain Ruby threads. Use
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}" # Float 0.0–1.0
788
- puts "Pass rate: #{metrics.pass_rate}" # fraction with score >= threshold
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 # any Phronomy::Tracing::Base subclass
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 current conversation or memory view can be advanced without deleting the canonical Journal:
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.clear_memory!
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 = nil # optional global before_llm_input hook
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); :cooperative (deprecated alias for :immediate)
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? # => true if called from inside a task
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`). Blocking I/O is isolated in `BlockingAdapterPool`.
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 observed by Phronomy.
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 Call.
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 canonical history. Instead, Phronomy selects the subset of available context needed for each LLM Call and records that selection in the Manifest.
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
- This distinction allows old history to remain available even when it does not fit in the current model's context window.
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
- 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.
1137
+ Per-call `before_llm_input` segment candidates also pass through Context Policy
1138
+ and are not written to the Journal.
1106
1139
 
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`.
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
- 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.
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 Policy APIs, deterministic persistent compaction, and other advanced policy extension points are still evolving and should not yet be treated as stable application APIs.
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
- tool-call batch, and after each `before_llm_input` hook. `CancellationError` is
1152
- raised immediately. Phronomy does not replay the complete Agent invocation. No threads are force-killed — `ensure`
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
- > - A single `KnowledgeSource#fetch` that is already blocking (e.g. HTTP call)
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 per-source or
1167
- > per-tool timeouts. Prefer library-native timeouts such as `Net::HTTP#read_timeout`,
1168
- > database `statement_timeout`, or Redis client timeout — these signal the I/O layer
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. This ensures the layer capable of safely aborting the I/O owns
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