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.
- checksums.yaml +4 -4
- data/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- 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 +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- 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/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- 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 +6 -7
- 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 +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +89 -190
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +49 -377
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- 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/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -265
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- 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
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- 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.
|