phronomy 0.16.0 → 0.18.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  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 +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -0,0 +1,122 @@
1
+ # ADR-013: Journal-backed Knowledge as Context Candidates
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ The Manifest-first Agent refactor established the Journal as the canonical
10
+ append-only record of Agent state and `LLMInputManifest` as the authority for
11
+ one provider call. The older Knowledge design remained outside that model:
12
+ `StaticKnowledge` and other `KnowledgeSource` objects were fetched separately,
13
+ then concatenated into the mandatory system prompt. That created several
14
+ problems:
15
+
16
+ - static/entity/RAG distinctions described acquisition strategy rather than
17
+ Context semantics;
18
+ - class-level and instance-level Knowledge followed different storage paths;
19
+ - persistent Knowledge did not share Agent persistence/reload semantics;
20
+ - Knowledge bypassed Context Policy and was always mandatory once configured;
21
+ - `static?`, `source`, fetch/caching APIs and EntityKnowledge behavior existed
22
+ primarily to support the obsolete source abstraction.
23
+
24
+ Applications still need two different lifetimes:
25
+
26
+ 1. information that becomes durable Agent Knowledge at creation time or later;
27
+ 2. request-scoped information used for only one LLM call.
28
+
29
+ ## Decision
30
+
31
+ Phronomy has one Knowledge Context category.
32
+
33
+ ### Persistent Knowledge
34
+
35
+ Persistent Knowledge is stored as ordinary append-only Journal records with
36
+ `kind: :knowledge`, `channel: :context`, `role: :user` and
37
+ `context_candidate: true`. Content lives in ContentStore and the Journal holds
38
+ its content reference and application metadata.
39
+
40
+ Agent instances accept Knowledge at creation and after creation:
41
+
42
+ ```ruby
43
+ agent = MyAgent.new(knowledge: ["Policy: ..."])
44
+ agent.add_knowledge("Customer locale: ja-JP")
45
+ ```
46
+
47
+ `Agent.load` requires no separate Knowledge source reconstruction because the
48
+ records are already persisted with the Agent.
49
+
50
+ ### Selection
51
+
52
+ Knowledge is not part of the public conversation transcript.
53
+ `JournalProjection` exposes active Knowledge together with active transcript
54
+ records for Context selection. `ContextCandidateResolver` and Context Policy
55
+ therefore handle persistent Knowledge through the same selection pipeline as
56
+ other optional Context.
57
+
58
+ Knowledge is optional by default. The fact that content is Knowledge does not
59
+ make it mandatory.
60
+
61
+ Selected Knowledge is materialized before ordinary conversation-history
62
+ segments so that persistent background Context is not interleaved into the
63
+ middle of dialogue chronology.
64
+
65
+ ### Reset semantics
66
+
67
+ `clear_knowledge!` appends a `knowledge_cleared` marker. Earlier Knowledge
68
+ records remain in the Journal but are excluded from later Context projections.
69
+ No Knowledge-generation counter is required.
70
+
71
+ `clear_transcript!` affects conversation history only. `reset_context!` resets
72
+ both transcript eligibility and Knowledge eligibility while retaining raw
73
+ Journal records.
74
+
75
+ ### Per-call Context
76
+
77
+ `before_llm_input` continues to accept `LLMInputPatch#segment_candidates`.
78
+ Those candidates are not persisted. They enter the same Context Policy request
79
+ as Journal-backed candidates and may be omitted when optional and over budget.
80
+
81
+ ### Acquisition responsibility
82
+
83
+ Phronomy core does not model `StaticKnowledge`, `EntityKnowledge`,
84
+ `RAGKnowledge` or `KnowledgeSource` subclasses. File loading, retrieval, entity
85
+ extraction and other acquisition strategies belong to applications or Tools.
86
+ Once an application chooses to retain the resulting information, it registers
87
+ plain logical Knowledge with the Agent.
88
+
89
+ ## Consequences
90
+
91
+ ### Positive
92
+
93
+ - one persistent representation and one Context-selection path;
94
+ - creation-time and post-creation Knowledge behave identically;
95
+ - durable Knowledge naturally survives Agent reload;
96
+ - token-budget selection can omit optional Knowledge without rewriting state;
97
+ - RAG/entity extraction can evolve independently of Agent Context persistence;
98
+ - no class-level static cache or `static?` distinction is needed.
99
+
100
+ ### Tradeoffs
101
+
102
+ - applications that previously declared class-level static Knowledge must pass
103
+ common Knowledge when constructing each Agent instance;
104
+ - source-specific refresh behavior is no longer a framework abstraction;
105
+ - provenance that matters to an application must be stored explicitly in
106
+ metadata rather than through a dedicated `source:` API.
107
+
108
+ ## Removed contracts
109
+
110
+ This decision removes the active contracts for:
111
+
112
+ - `Phronomy::KnowledgeSource`;
113
+ - `Agent::Context::Knowledge::Base`;
114
+ - `StaticKnowledge`;
115
+ - `EntityKnowledge`;
116
+ - `static_knowledge`, `static_knowledge_sources`, `static_knowledge_chunks`,
117
+ `static_knowledge_refresh!`;
118
+ - `add_knowledge_source`, `instance_knowledge_chunks`;
119
+ - `clear_memory!` / `memory_generation` as obsolete Agent Context concepts.
120
+
121
+ ADR-005 is superseded by this decision. ADR-012 remains the authority for the
122
+ Journal/Manifest separation and Context Policy model.
data/docs/features.md ADDED
@@ -0,0 +1,87 @@
1
+ # Features and API stability
2
+
3
+ API means Application Programming Interface in this document.
4
+
5
+ Phronomy is pre-1.0. Minor releases may include breaking changes even to APIs
6
+ labelled Stable; patch releases should remain non-breaking. Consult
7
+ [`CHANGELOG.md`](../CHANGELOG.md) when upgrading.
8
+
9
+ - **Stable** — API is considered complete and suitable for production use.
10
+ - **Beta** — functionality is complete and tested, but signatures or behaviour may evolve in a minor release.
11
+ - **Experimental** — functionality may change without notice and is not recommended as a long-term compatibility boundary.
12
+
13
+ The `main` branch contains unreleased development work. Pin a released gem version
14
+ for production deployments.
15
+
16
+ ## Core building blocks
17
+
18
+ | Feature | Stability |
19
+ |---|---|
20
+ | **Workflow** — Stateful, branching workflows with `wait_state` and explicit events | Stable |
21
+ | **Agent** — Stateful ReAct-style agents with stable `agent_id`, persistence-backed execution state, canonical history, guardrails, and conversation context | Stable |
22
+ | **Before-Large-Language-Model (LLM) Input Hook** — Three-tier per-call LLM input customization via `before_llm_input` and `LLMInputPatch` | Stable |
23
+ | **Context Management** — Journal + Context Policy + per-LLM-call Manifest with token-budget-aware selection and protocol-safe Tool Call / Tool message dependencies | Stable |
24
+ | **Filters** — Input/output transformation and blocking via `Filter::Base` | Beta |
25
+ | **`PromptInjectionFilter`** — Built-in pattern-based prompt-injection filter | Beta |
26
+ | **Capability redaction/result-size controls** — `redact_params` and `max_result_size` | Beta |
27
+ | **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
28
+ | **Tracing** — Pluggable span-based observability | Stable |
29
+ | **Error Taxonomy** — Provider errors translated to Phronomy transport/authentication/rate-limit/context errors | Beta |
30
+
31
+ ## Knowledge and integration
32
+
33
+ | Feature | Stability |
34
+ |---|---|
35
+ | **Knowledge** — Journal-backed persistent Agent context registered with `knowledge:` / `add_knowledge`, selected per LLM call by Context Policy | Beta |
36
+ | **`VectorStore#size`** — Document count for InMemory, RedisSearch, and Pgvector backends | Beta |
37
+ | **`VectorStore::AsyncBackend`** — Pluggable async VectorStore interface with pool-backed defaults and native-async override points | Beta |
38
+ | **Model Context Protocol (MCP) Tool** — `Phronomy::Tools::Mcp` integration through the official `mcp` gem | Beta |
39
+ | **Agent Tool** — `Phronomy::Tools::Agent.from_agent` exposes a child Agent as a Tool without occupying a worker while waiting | Beta |
40
+ | **Vector Search Tool** — `Phronomy::Tools::VectorSearch` wraps VectorStore and Embeddings adapters | Beta |
41
+
42
+ ## Execution and reliability
43
+
44
+ | Feature | Stability |
45
+ |---|---|
46
+ | **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions | Beta |
47
+ | **`invoke` / `invoke_async`** — Blocking and non-blocking Agent/Workflow entry points | Stable |
48
+ | **Agent async events** — `invoke_async(..., on_event:)` and `stream_async(..., on_event:)`; streaming additionally emits `:token` | Beta |
49
+ | **`stream` / `stream_async`** — Event callbacks execute on EventLoop and must return quickly | Beta |
50
+ | **`stream_callback_error_policy`** — Terminal event callback error policy (`:report` / `:fail_task`) | Beta |
51
+ | **`Task#map`** — Application-level Task result transformation and error propagation | Stable |
52
+ | **CancellationToken** — Cooperative cancellation with explicit `cancel!`, lazy monotonic deadlines, and callback registration | Experimental |
53
+ | **Tool `execution_mode`** — `:cooperative` for short EventLoop-safe work; `:offloaded` for synchronous work that must stay off EventLoop | Experimental |
54
+ | **OffloadPool sizing** — `offload_pool_size` / `offload_queue_size`; named pools available for application-owned isolation | Beta |
55
+ | **InvocationContext** — Explicit correlation/cancellation/deadline context for Agent and Workflow invocations | Beta |
56
+ | **Metrics** — OffloadPool active/queue/abandoned metrics plus EventLoop queue/lag metrics | Beta |
57
+ | **Runtime lifecycle** — Runtime-owned EventLoop and terminal `Runtime#shutdown` | Beta |
58
+
59
+ ## Agent and workflow patterns
60
+
61
+ | Feature | Stability |
62
+ |---|---|
63
+ | **Workflow asynchronous pattern** — Start async work, return immediately, and continue through `Workflow#signal` | Beta |
64
+ | **Multi-agent** — Agent-as-Tool and hub-and-spoke handoff routing | Beta |
65
+ | **GeneratorVerifier** — Generator-Verifier loop with injectable prompts/parsers | Beta |
66
+ | **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
67
+ | **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
68
+ | **SharedState** — Peer-agent shared-state coordination | Experimental |
69
+ | **Human-in-the-loop approval** — Suspension and approval/resume of Tool requests | Beta |
70
+ | **`tool_approval_policy`** — Application-defined allow/approve/reject policy | Beta |
71
+
72
+ ## Public API boundary
73
+
74
+ The feature tables above describe the primary APIs intended for gem consumers.
75
+ Source declarations marked `@api private`, including most EventLoop/FSMSession and
76
+ OffloadPool internals, are implementation details and may change without the same
77
+ compatibility guarantees.
78
+
79
+ ## Advanced and internal APIs
80
+
81
+ | Feature | Stability |
82
+ |---|---|
83
+ | **`Phronomy::Diagnostics`** — Snapshot of EventLoop lag/queue state and OffloadPool activity | Experimental |
84
+ | **`Phronomy::Testing::FakeClock`** — Test-only deterministic clock helper | Beta |
85
+
86
+ For runtime ownership and the distinction between public lifecycle APIs and
87
+ private execution machinery, see [Runtime and concurrency](runtime-and-concurrency.md).
@@ -0,0 +1,351 @@
1
+ # Getting started
2
+
3
+ This guide contains the setup and introductory examples that were previously
4
+ embedded in the repository README. The README remains the project entry point;
5
+ this document carries the longer operational examples.
6
+
7
+ ## Install
8
+
9
+ Add Phronomy to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "phronomy"
13
+ ```
14
+
15
+ Then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ Phronomy uses RubyLLM for Large Language Model (LLM) access. Configure provider credentials and the
22
+ transport retry/timeout policy on RubyLLM itself:
23
+
24
+ ```ruby
25
+ RubyLLM.configure do |c|
26
+ c.openai_api_key = ENV["OPENAI_API_KEY"]
27
+ # c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
28
+
29
+ c.request_timeout = 120
30
+ c.max_retries = 3
31
+ c.retry_interval = 0.1
32
+ c.retry_backoff_factor = 2
33
+ c.retry_interval_randomness = 0.5
34
+ end
35
+ ```
36
+
37
+ Phronomy does not add a second LLM transport timeout/retry layer on top of the
38
+ configured adapter.
39
+
40
+ ## Optional dependencies
41
+
42
+ Install only the backend gems required by your application:
43
+
44
+ | Gem | Required for |
45
+ |---|---|
46
+ | `pgvector` | `Phronomy::VectorStore::Pgvector` |
47
+ | `redis` | `Phronomy::VectorStore::RedisSearch` |
48
+ | `opentelemetry-api` | `Phronomy::Tracing::OpenTelemetryTracer` |
49
+
50
+ ## Define a Tool and Agent
51
+
52
+ ```ruby
53
+ class WebSearch < Phronomy::Agent::Context::Capability::Base
54
+ description "Search the web"
55
+ param :query, type: :string, desc: "Search query"
56
+
57
+ def execute(query:)
58
+ "Mock search result for: #{query}"
59
+ end
60
+ end
61
+
62
+ class ResearchAgent < Phronomy::Agent::Base
63
+ agent_definition id: "research-agent", version: 1
64
+ model "gpt-4o"
65
+ instructions "You are a research assistant. Use tools to answer questions."
66
+ tools(WebSearch => nil)
67
+ max_iterations 5
68
+ end
69
+
70
+ result = ResearchAgent.new.invoke("Research Ruby AI frameworks")
71
+ puts result[:output]
72
+ ```
73
+
74
+ Every concrete stateful Agent definition declares a stable definition ID and
75
+ version. The definition identity is checked when persisted Agent state is loaded.
76
+
77
+ ## Stateful Agent persistence
78
+
79
+ Phronomy Agents own their conversation history and persistent Knowledge. The
80
+ application does not need to pass the previous `messages` array back into every
81
+ invocation.
82
+
83
+ ```ruby
84
+ persistence = Phronomy::Persistence::InMemory.new
85
+
86
+ agent = ResearchAgent.create(
87
+ agent_id: "research-session-42",
88
+ knowledge: ["Customer tier: enterprise"],
89
+ persistence: persistence
90
+ )
91
+
92
+ agent.invoke("My name is Alice.")
93
+ agent.add_knowledge("Customer locale: ja-JP")
94
+ result = agent.invoke("What is my name?")
95
+
96
+ puts result[:output]
97
+ ```
98
+
99
+ Load the same Agent again when the same Persistence backend is available:
100
+
101
+ ```ruby
102
+ agent = ResearchAgent.load(
103
+ "research-session-42",
104
+ persistence: persistence
105
+ )
106
+
107
+ agent.invoke("Continue our previous discussion.")
108
+ ```
109
+
110
+ The active transcript and Knowledge views can be advanced independently without
111
+ deleting the append-only canonical Journal:
112
+
113
+ ```ruby
114
+ agent.clear_transcript!
115
+ agent.clear_knowledge!
116
+ agent.reset_context!
117
+ ```
118
+
119
+ `purge!` is different: it permanently removes the Agent and persisted execution
120
+ history from the configured Persistence backend.
121
+
122
+ ## Sync and async Agent APIs
123
+
124
+ Use synchronous APIs at an external/top-level application boundary and async
125
+ APIs when the caller must remain non-blocking.
126
+
127
+ ```ruby
128
+ result = agent.invoke("Hello")
129
+
130
+ task = agent.invoke_async("Hello")
131
+ result = task.wait_result
132
+ ```
133
+
134
+ `Task#wait_result` is for an external caller. Do not block EventLoop waiting for
135
+ a Task that can only complete through that same EventLoop.
136
+
137
+ Streaming follows the same split:
138
+
139
+ ```ruby
140
+ agent.stream("Explain the design") do |event|
141
+ puts event.payload if event.type == :token
142
+ end
143
+ ```
144
+
145
+ ```ruby
146
+ task = agent.stream_async(
147
+ "Explain the design",
148
+ on_event: ->(event) { puts event.payload if event.type == :token }
149
+ )
150
+ ```
151
+
152
+ Streaming callbacks execute on EventLoop and therefore should return quickly.
153
+
154
+ ## Human-in-the-loop approval
155
+
156
+ A Tool requiring approval can suspend an Agent invocation. Resume it with the
157
+ approval request identifier returned by the suspension result.
158
+
159
+ At a top-level synchronous boundary:
160
+
161
+ ```ruby
162
+ result = agent.invoke("Perform the requested protected action")
163
+
164
+ if result[:suspended]
165
+ request = result[:approval_request]
166
+ result = agent.approve(
167
+ result[:execution_id],
168
+ approval_request_id: request.id,
169
+ approved: true
170
+ )
171
+ end
172
+ ```
173
+
174
+ From an EventLoop callback, use `approve_async` rather than blocking EventLoop.
175
+
176
+ ## Workflow basics
177
+
178
+ A Workflow is state-machine-driven and can halt at an explicit wait state:
179
+
180
+ ```ruby
181
+ class ReviewContext
182
+ include Phronomy::WorkflowContext
183
+ field :draft, type: :replace
184
+ field :feedback, type: :replace
185
+ field :approved, type: :replace, default: false
186
+ end
187
+
188
+ write_draft = ->(state) { state.merge(draft: "Draft content") }
189
+ review_draft = ->(state) { state.merge(feedback: "Feedback on: #{state.draft}") }
190
+
191
+ workflow = Phronomy::Workflow.define(ReviewContext) do
192
+ initial :write
193
+ state :write, action: write_draft
194
+ state :review, action: review_draft
195
+ wait_state :awaiting_approval
196
+ state :finalize, action: ->(s) { s.merge(approved: true) }
197
+
198
+ transition from: :write, to: :review
199
+ transition from: :review, to: :awaiting_approval
200
+ transition from: :awaiting_approval, on: :approve, to: :finalize
201
+ transition from: :awaiting_approval, on: :reject, to: :write
202
+ transition from: :finalize, to: :__finish__
203
+ end
204
+
205
+ state = workflow.invoke({draft: ""}, config: {thread_id: "doc-1"})
206
+ final = workflow.send_event(state: state, event: :approve)
207
+ puts final.approved
208
+ ```
209
+
210
+ Workflow entry and transition actions are synchronous Run-to-Completion
211
+ callbacks. If a Workflow needs an Agent or another asynchronous lifecycle, start
212
+ it asynchronously, return the Workflow context immediately, and deliver its
213
+ completion later with `Workflow#signal`.
214
+
215
+ ```ruby
216
+ class AnswerContext
217
+ include Phronomy::WorkflowContext
218
+
219
+ field :question, type: :replace, default: ""
220
+ field :answer, type: :replace, default: nil
221
+ field :thread_id, type: :replace, default: nil
222
+ end
223
+
224
+ class ResearchAgent < Phronomy::Agent::Base
225
+ agent_definition id: "research-agent", version: 1
226
+ model "gpt-4o-mini"
227
+ instructions "Research the question and return a concise answer."
228
+ end
229
+
230
+ my_agent = ResearchAgent.new
231
+ workflow = nil
232
+
233
+ workflow = Phronomy::Workflow.define(AnswerContext) do
234
+ initial :asking
235
+ state :asking
236
+ state :done
237
+
238
+ entry :asking, ->(ctx) {
239
+ thread_id = ctx.thread_id
240
+
241
+ my_agent.invoke_async(
242
+ ctx.question,
243
+ on_event: ->(event) {
244
+ next unless event.type == :done
245
+
246
+ workflow.signal(
247
+ thread_id: thread_id,
248
+ event: :answer_ready,
249
+ payload: {answer: event.payload[:output]}
250
+ )
251
+ }
252
+ )
253
+
254
+ ctx
255
+ }
256
+
257
+ transition(
258
+ from: :asking,
259
+ on: :answer_ready,
260
+ to: :done,
261
+ action: ->(ctx, event) { ctx.merge(answer: event.payload[:answer]) }
262
+ )
263
+ end
264
+ ```
265
+
266
+ Returning a `Phronomy::Task` from a Workflow entry/transition action is not an
267
+ implicit await mechanism and is rejected.
268
+
269
+ ## Agent as Tool
270
+
271
+ Expose a child Agent using `Phronomy::Tools::Agent.from_agent` rather than calling
272
+ a synchronous child Agent from a Tool worker:
273
+
274
+ ```ruby
275
+ ResearchTool = Phronomy::Tools::Agent.from_agent(
276
+ ResearchAgent,
277
+ tool_name: "research",
278
+ description: "Delegate research to the research Agent"
279
+ )
280
+
281
+ class OrchestratorAgent < Phronomy::Agent::Base
282
+ agent_definition id: "orchestrator-agent", version: 1
283
+ model "gpt-4o"
284
+ instructions "Use the research Tool when research is required."
285
+ tools(ResearchTool => nil)
286
+ end
287
+ ```
288
+
289
+ Agent-backed Tools return control to EventLoop while the child lifecycle is
290
+ waiting. They do not occupy an OffloadPool worker merely to wait for a child
291
+ Agent result.
292
+
293
+ ## Filters
294
+
295
+ Filters can transform or reject values at Agent boundaries:
296
+
297
+ ```ruby
298
+ class NoCreditCardFilter < Phronomy::Filter::Base
299
+ def call(value, **_context)
300
+ block!("Credit card numbers are not allowed") if value.match?(/\d{4}-\d{4}-\d{4}-\d{4}/)
301
+ value
302
+ end
303
+ end
304
+
305
+ agent.add_input_filter(NoCreditCardFilter.new)
306
+ ```
307
+
308
+ Phronomy includes `PromptInjectionFilter` as a baseline pattern filter. It is not
309
+ a complete security policy for untrusted input.
310
+
311
+ ## Persistent Knowledge and per-call context
312
+
313
+ Register durable Knowledge on the Agent:
314
+
315
+ ```ruby
316
+ agent.add_knowledge(
317
+ "Customer locale: ja-JP",
318
+ metadata: {"origin" => "customer_profile"}
319
+ )
320
+ ```
321
+
322
+ Request-scoped context can instead be supplied through `before_llm_input` using
323
+ `LLMInputPatch#segment_candidates`; those candidates enter Context Policy for the
324
+ specific call and are not persisted to the Journal.
325
+
326
+ ## Model Context Protocol (MCP)
327
+
328
+ Phronomy targets MCP 1.x through the official `mcp` gem:
329
+
330
+ ```ruby
331
+ search_tool = Phronomy::Tools::Mcp.from_server(
332
+ "stdio://./mcp-server",
333
+ tool_name: "web_search"
334
+ )
335
+
336
+ begin
337
+ # use search_tool
338
+ ensure
339
+ search_tool.close
340
+ end
341
+ ```
342
+
343
+ See [MCP client](mcp-client.md) for schema, error, cancellation, and lifecycle
344
+ contracts.
345
+
346
+ ## Next steps
347
+
348
+ - [Features and API stability](features.md)
349
+ - [Runtime and concurrency](runtime-and-concurrency.md)
350
+ - [Architecture decisions](decisions/)
351
+ - [Migration guides](migrations/)
@@ -0,0 +1,35 @@
1
+ # Migration from 0.15-era execution-policy APIs
2
+
3
+ This guide preserves the execution-policy migration information that previously
4
+ lived in the top-level README. It is historical upgrade guidance; new code should
5
+ follow the current contracts documented in
6
+ [Runtime and concurrency](../runtime-and-concurrency.md).
7
+
8
+ | Removed Phronomy setting/API | Replacement |
9
+ |---|---|
10
+ | `retry_policy` | RubyLLM transport retry, or explicit application orchestration |
11
+ | `invoke_timeout` | `InvocationContext#deadline` or `cancellation_token` when the caller needs a root deadline |
12
+ | `config[:llm_timeout]` | `RubyLLM.configure { |c| c.request_timeout = ... }` |
13
+ | Tool `retry_on` | Tool/client-specific retry with explicit idempotency guarantees |
14
+ | `config[:tool_timeout]` | Tool/client-native timeout |
15
+ | `max_parallel_tools` | No replacement; `parallel_tool_execution` remains an on/off mode |
16
+ | `InvocationContext#provider_limits` | Configure the provider client directly |
17
+ | `stream_queue_max_size` | No replacement; the shared EventLoop queue is unbounded by design. Monitor `Metrics.snapshot[:event_loop_queue_depth]` instead |
18
+
19
+ ## Timeout ownership
20
+
21
+ Phronomy owns logical lifecycle deadlines and cooperative cancellation. Provider,
22
+ HTTP, database, MCP, and other transport-level timeouts belong to the relevant
23
+ client library.
24
+
25
+ A caller-defined root deadline can be represented through `InvocationContext` or
26
+ a cancellation token, but that does not replace operation-native socket/request/
27
+ statement timeouts for a synchronous call already in progress.
28
+
29
+ ## Retry ownership
30
+
31
+ Automatic replay of an entire Agent invocation is not a generic framework retry
32
+ policy. Configure transport retry on RubyLLM or another adapter, and implement
33
+ application orchestration explicitly when replay is semantically safe.
34
+
35
+ See [`CHANGELOG.md`](../../CHANGELOG.md) for the release-by-release history.
@@ -0,0 +1,43 @@
1
+ # 0.16 cleanup migration
2
+
3
+ This guide preserves the compatibility cleanup information that previously lived
4
+ in the top-level README. It is intended for applications upgrading older Phronomy
5
+ code; new applications should use the current documentation directly.
6
+
7
+ | Removed API | Current contract |
8
+ |---|---|
9
+ | `context_overhead` | Manifest-first assembly budgets actual mandatory + selected content |
10
+ | Tool `on_error :return_empty` | Use `:raise` or `:suppress` |
11
+ | `dispatch_parallel(..., force_kill:)` / `fan_out(..., force_kill:)` | Cooperative cancellation; no force-kill switch |
12
+ | `runtime_backend` | Removed. Phronomy has one control model: EventLoop/FSMSession for lifecycle coordination and OffloadPool for synchronous work that must stay off EventLoop |
13
+ | `Runtime.instance = ...` | Runtime replacement is test/internal infrastructure, not a public setter |
14
+ | `Runtime#spawn` / `TaskGroup` | Removed. Start framework async work through domain async APIs (`invoke_async`, Workflow events, ToolInvocation, FanOut) |
15
+ | `tools ToolA, ToolB` | Use `tools(ToolA => nil, ToolB => nil)` |
16
+ | `CancellationToken.new(deadline: Time...)` | Use `CancellationToken.timeout_after(seconds)` for a lazy token deadline, or a Runtime-backed cancellation scope when callback delivery on deadline is required |
17
+ | `StaticKnowledge` / `EntityKnowledge` / `Knowledge::Base` / `Phronomy::KnowledgeSource` | Register plain persistent Knowledge with `knowledge:` or `add_knowledge` |
18
+ | `static_knowledge*` class APIs | Persistent Knowledge belongs to Agent instances and is Journal-backed |
19
+ | `clear_memory!` | Use `clear_knowledge!`; conversation history is controlled independently with `clear_transcript!` |
20
+
21
+ The legacy `build_context` / `LlmContextWindow::Assembler` extension path is no
22
+ longer an active API. Stateful Agent input is assembled through the canonical:
23
+
24
+ ```text
25
+ Journal
26
+ → Context candidates
27
+ → Context Policy
28
+ → Large Language Model (LLM) Input Manifest
29
+ → runtime projection
30
+ ```
31
+
32
+ ## Concurrency model
33
+
34
+ The active concurrency model is EventLoop/FSMSession for logical lifecycle
35
+ coordination plus OffloadPool for synchronous work that cannot run on EventLoop.
36
+ There is no configurable runtime backend and no generic framework task-spawn
37
+ backend.
38
+
39
+ For the current design, see
40
+ [Runtime and concurrency](../runtime-and-concurrency.md) and
41
+ [ADR-010](../decisions/010-cooperative-first-concurrency.md).
42
+
43
+ See [`CHANGELOG.md`](../../CHANGELOG.md) for release details.