phronomy 0.6.0 → 0.7.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 +21 -0
- data/CHANGELOG.md +338 -0
- data/CONTRIBUTING.md +102 -0
- data/README.md +242 -27
- data/RELEASE_CHECKLIST.md +86 -0
- data/SECURITY.md +80 -0
- data/benchmark/baseline.json +9 -0
- data/benchmark/bench_agent_invoke.rb +105 -0
- data/benchmark/bench_context_assembler.rb +46 -0
- data/benchmark/bench_regression.rb +171 -0
- data/benchmark/bench_token_estimator.rb +44 -0
- data/benchmark/bench_tool_schema.rb +69 -0
- data/benchmark/bench_vector_store.rb +39 -0
- data/benchmark/bench_workflow.rb +55 -0
- data/benchmark/run_all.rb +118 -0
- data/docs/decisions/001-rubyllm-as-provider-layer.md +42 -0
- data/docs/decisions/002-workflow-context-immutability.md +42 -0
- data/docs/decisions/003-event-loop-singleton.md +48 -0
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +51 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +45 -0
- data/docs/decisions/006-no-built-in-guardrails.md +48 -0
- data/docs/decisions/007-mcp-is-beta-stability.md +51 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +52 -0
- data/docs/decisions/009-state-store-abstraction.md +141 -0
- data/lib/phronomy/agent/base.rb +194 -12
- data/lib/phronomy/agent/before_completion_context.rb +1 -0
- data/lib/phronomy/agent/checkpoint.rb +1 -0
- data/lib/phronomy/agent/concerns/before_completion.rb +6 -0
- data/lib/phronomy/agent/concerns/error_translation.rb +45 -0
- data/lib/phronomy/agent/concerns/guardrailable.rb +3 -0
- data/lib/phronomy/agent/concerns/retryable.rb +12 -1
- data/lib/phronomy/agent/concerns/suspendable.rb +4 -0
- data/lib/phronomy/agent/fsm.rb +15 -0
- data/lib/phronomy/agent/handoff.rb +3 -0
- data/lib/phronomy/agent/orchestrator.rb +123 -11
- data/lib/phronomy/agent/parallel_tool_chat.rb +21 -4
- data/lib/phronomy/agent/react_agent.rb +8 -6
- data/lib/phronomy/agent/runner.rb +2 -0
- data/lib/phronomy/agent/shared_state.rb +11 -0
- data/lib/phronomy/agent/suspend_signal.rb +2 -0
- data/lib/phronomy/agent/team_coordinator.rb +17 -5
- data/lib/phronomy/cancellation_token.rb +92 -0
- data/lib/phronomy/configuration.rb +26 -2
- data/lib/phronomy/context/assembler.rb +6 -0
- data/lib/phronomy/context/compaction_context.rb +2 -0
- data/lib/phronomy/context/context_version_cache.rb +2 -0
- data/lib/phronomy/context/token_budget.rb +3 -0
- data/lib/phronomy/context/token_estimator.rb +9 -2
- data/lib/phronomy/context/trigger_context.rb +1 -0
- data/lib/phronomy/context/trim_context.rb +4 -0
- data/lib/phronomy/embeddings/base.rb +5 -2
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +6 -2
- data/lib/phronomy/eval/comparison.rb +2 -0
- data/lib/phronomy/eval/dataset.rb +4 -0
- data/lib/phronomy/eval/metrics.rb +6 -0
- data/lib/phronomy/eval/runner.rb +2 -0
- data/lib/phronomy/eval/scorer/base.rb +1 -0
- data/lib/phronomy/eval/scorer/exact_match.rb +2 -0
- data/lib/phronomy/eval/scorer/includes_scorer.rb +2 -0
- data/lib/phronomy/eval/scorer/llm_judge.rb +2 -0
- data/lib/phronomy/event_loop.rb +114 -7
- data/lib/phronomy/fsm_session.rb +8 -1
- data/lib/phronomy/generator_verifier.rb +2 -0
- data/lib/phronomy/guardrail/base.rb +3 -0
- data/lib/phronomy/knowledge_source/base.rb +6 -2
- data/lib/phronomy/knowledge_source/entity_knowledge.rb +7 -2
- data/lib/phronomy/knowledge_source/rag_knowledge.rb +8 -4
- data/lib/phronomy/knowledge_source/static_knowledge.rb +7 -2
- data/lib/phronomy/loader/base.rb +1 -0
- data/lib/phronomy/loader/csv_loader.rb +2 -0
- data/lib/phronomy/loader/markdown_loader.rb +2 -0
- data/lib/phronomy/loader/plain_text_loader.rb +1 -0
- data/lib/phronomy/output_parser/base.rb +1 -0
- data/lib/phronomy/output_parser/json_parser.rb +22 -3
- data/lib/phronomy/output_parser/structured_parser.rb +2 -0
- data/lib/phronomy/prompt_template.rb +5 -0
- data/lib/phronomy/runnable.rb +20 -3
- data/lib/phronomy/splitter/base.rb +2 -0
- data/lib/phronomy/splitter/fixed_size_splitter.rb +2 -0
- data/lib/phronomy/splitter/recursive_splitter.rb +2 -0
- data/lib/phronomy/state_store/base.rb +48 -0
- data/lib/phronomy/state_store/in_memory.rb +62 -0
- data/lib/phronomy/tool/agent_tool.rb +1 -0
- data/lib/phronomy/tool/base.rb +189 -27
- data/lib/phronomy/tool/mcp_tool.rb +68 -13
- data/lib/phronomy/tracing/base.rb +3 -0
- data/lib/phronomy/tracing/langfuse_tracer.rb +2 -0
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +2 -0
- data/lib/phronomy/vector_store/base.rb +33 -7
- data/lib/phronomy/vector_store/in_memory.rb +16 -7
- data/lib/phronomy/vector_store/pgvector.rb +40 -9
- data/lib/phronomy/vector_store/redis_search.rb +29 -8
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +96 -7
- data/lib/phronomy/workflow_context.rb +54 -4
- data/lib/phronomy/workflow_runner.rb +35 -7
- data/lib/phronomy.rb +70 -1
- data/scripts/api_snapshot.rb +91 -0
- data/scripts/check_api_annotations.rb +68 -0
- data/scripts/check_private_enforcement.rb +93 -0
- data/scripts/check_readme_runnable.rb +98 -0
- data/scripts/run_mutation.sh +46 -0
- metadata +45 -2
data/README.md
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# Phronomy
|
|
2
2
|
|
|
3
|
+
> **⚠️ Development Notice**
|
|
4
|
+
> This project is primarily developed and maintained by **AI coding agents**.
|
|
5
|
+
> As a result, `main` receives frequent, large, and unannounced changes.
|
|
6
|
+
> External contributors should expect significant churn and potential conflicts at any time.
|
|
7
|
+
> We apologise for the instability this may cause.
|
|
8
|
+
|
|
3
9
|
**Phronomy** is a Ruby AI agent framework inspired by open-source AI agent frameworks.
|
|
4
10
|
It provides composable building blocks — Workflows, Agents, Tools, Guardrails, RAG, and Tracing — all powered by [RubyLLM](https://github.com/crmne/ruby_llm) for LLM abstraction.
|
|
5
11
|
|
|
6
12
|
## Features
|
|
7
13
|
|
|
8
|
-
> **Stability labels
|
|
9
|
-
>
|
|
10
|
-
> `
|
|
14
|
+
> **Stability labels** (phronomy is pre-1.0, so `0.x` minor releases may include
|
|
15
|
+
> breaking changes even to `Stable` APIs; patch releases (`0.x.y`) are non-breaking):
|
|
16
|
+
> - `Stable` — API is considered complete and suitable for production use. Breaking changes
|
|
17
|
+
> within a minor release are avoided, and any breaking changes in a minor bump are noted
|
|
18
|
+
> in `CHANGELOG.md`.
|
|
19
|
+
> - `Beta` — Functionality is complete and tested, but the API may change in a minor version release (0.x). Use with awareness that signatures or behaviour may evolve.
|
|
20
|
+
> - `Experimental` — Functionality may be incomplete or subject to breaking changes at any time without notice. Not recommended for production use.
|
|
21
|
+
>
|
|
22
|
+
> **Note**: The `main` branch contains unreleased development work. Pin to a released gem
|
|
23
|
+
> version (`gem "phronomy", "~> 0.x"`) for stability in production.
|
|
11
24
|
|
|
12
25
|
| Feature | Stability |
|
|
13
26
|
|---|---|
|
|
@@ -18,17 +31,22 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
18
31
|
| **Agent** — ReAct-style tool-calling agents with guardrails and conversation history | Stable |
|
|
19
32
|
| **Before-Completion Hook** — Three-tier LLM parameter injection | Stable |
|
|
20
33
|
| **Context Management** — Token budget calculation, estimation, and pruning | Stable |
|
|
21
|
-
| **Knowledge/RAG** — Retrieval sources with pluggable loaders, splitters, and vector stores | Beta |
|
|
34
|
+
| **Knowledge/RAG** — Retrieval sources with pluggable loaders, splitters, and vector stores; `static_knowledge_refresh!` for runtime cache invalidation | Beta |
|
|
35
|
+
| **`VectorStore#size`** — Returns document count for all three backends (InMemory, RedisSearch, Pgvector) | Beta |
|
|
22
36
|
| **Multi-agent** — Agent-as-Tool pattern and hub-and-spoke handoff routing | Beta |
|
|
23
37
|
| **GeneratorVerifier** — Generator-Verifier loop with injectable prompt builders/parsers | Beta |
|
|
24
38
|
| **Agent::Orchestrator** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
25
|
-
| **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + stateful
|
|
39
|
+
| **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + stateful workers with sequential task assignment (worker-local message history persisted across tasks) | Beta |
|
|
26
40
|
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
27
41
|
| **Guardrails** — Input/output validation with custom `InputGuardrail`/`OutputGuardrail` | Beta |
|
|
28
42
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
29
43
|
| **Eval Framework** — Dataset-driven evaluation with multiple scorer types | Beta |
|
|
30
44
|
| **Tracing** — Pluggable span-based observability | Stable |
|
|
31
45
|
| **MCP Tool** — Model Context Protocol server integration | Beta |
|
|
46
|
+
| **Error Taxonomy** — `RateLimitError`, `AuthenticationError`, `ContextLengthError`, `TransportError` (subclasses of `Phronomy::Error`) raised at the agent retry boundary | Beta |
|
|
47
|
+
| **`Phronomy.with_configuration` / `Phronomy.reset_runtime!`** — Scoped configuration override and full runtime reset for test isolation | Beta |
|
|
48
|
+
| **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 | Experimental |
|
|
49
|
+
| **`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 |
|
|
32
50
|
|
|
33
51
|
## Installation
|
|
34
52
|
|
|
@@ -44,17 +62,42 @@ Then run:
|
|
|
44
62
|
bundle install
|
|
45
63
|
```
|
|
46
64
|
|
|
65
|
+
### RubyLLM setup
|
|
66
|
+
|
|
67
|
+
Phronomy uses [RubyLLM](https://github.com/crmne/ruby_llm) for LLM access.
|
|
68
|
+
Configure your provider credentials before using agents or chains:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RubyLLM.configure do |c|
|
|
72
|
+
c.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
73
|
+
# c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See the [RubyLLM documentation](https://rubyllm.com) for all supported providers.
|
|
78
|
+
|
|
79
|
+
### Optional dependencies
|
|
80
|
+
|
|
81
|
+
Install additional gems only for the features you use:
|
|
82
|
+
|
|
83
|
+
| Gem | Required for |
|
|
84
|
+
|-----|-------------|
|
|
85
|
+
| `pgvector` | `Phronomy::VectorStore::Pgvector` |
|
|
86
|
+
| `redis` | `Phronomy::VectorStore::RedisSearch` |
|
|
87
|
+
| `opentelemetry-api` | `Phronomy::Tracing::OpenTelemetryTracer` |
|
|
88
|
+
|
|
47
89
|
## Quick Start
|
|
48
90
|
|
|
49
91
|
### Agent — ReAct tool-calling agent
|
|
50
92
|
|
|
51
|
-
```ruby
|
|
93
|
+
```ruby runnable
|
|
52
94
|
class WebSearch < Phronomy::Tool::Base
|
|
53
95
|
description "Search the web"
|
|
54
96
|
param :query, type: :string, desc: "Search query"
|
|
55
97
|
|
|
56
98
|
def execute(query:)
|
|
57
|
-
#
|
|
99
|
+
# Replace with a real search API call (e.g., SerpAPI, Tavily)
|
|
100
|
+
"Mock search result for: #{query}"
|
|
58
101
|
end
|
|
59
102
|
end
|
|
60
103
|
|
|
@@ -71,7 +114,7 @@ puts result[:output]
|
|
|
71
114
|
|
|
72
115
|
### Workflow — Stateful workflow with wait_state/send_event
|
|
73
116
|
|
|
74
|
-
```ruby
|
|
117
|
+
```ruby runnable
|
|
75
118
|
class ReviewContext
|
|
76
119
|
include Phronomy::WorkflowContext
|
|
77
120
|
field :draft, type: :replace
|
|
@@ -79,10 +122,14 @@ class ReviewContext
|
|
|
79
122
|
field :approved, type: :replace, default: false
|
|
80
123
|
end
|
|
81
124
|
|
|
125
|
+
# Placeholder callables representing your own implementation
|
|
126
|
+
write_draft = ->(state) { state.merge(draft: "Draft content here") }
|
|
127
|
+
review_draft = ->(state) { state.merge(feedback: "Feedback on: #{state.draft}") }
|
|
128
|
+
|
|
82
129
|
app = Phronomy::Workflow.define(ReviewContext) do
|
|
83
130
|
initial :write
|
|
84
|
-
state :write, action:
|
|
85
|
-
state :review, action:
|
|
131
|
+
state :write, action: write_draft
|
|
132
|
+
state :review, action: review_draft
|
|
86
133
|
wait_state :awaiting_approval # halts here for human decision
|
|
87
134
|
state :finalize, action: ->(s) { s.merge(approved: true) }
|
|
88
135
|
transition from: :write, to: :review
|
|
@@ -102,6 +149,19 @@ final = app.send_event(state: state, event: :approve)
|
|
|
102
149
|
puts "Approved: #{final.approved}" # => true
|
|
103
150
|
```
|
|
104
151
|
|
|
152
|
+
In EventLoop mode (`c.event_loop = true`), `Agent#run_as_child` spawns a child agent
|
|
153
|
+
asynchronously. When the child succeeds, `:child_completed` is dispatched; when it fails,
|
|
154
|
+
`:child_failed` is dispatched. Always declare both transitions to avoid a stuck workflow:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# EventLoop mode: workflow that runs an agent as a child FSM
|
|
158
|
+
entry :run_agent, ->(ctx) {
|
|
159
|
+
MyAgent.new.run_as_child(ctx.query, ctx: ctx) { |r| ctx.answer = r[:output] }
|
|
160
|
+
}
|
|
161
|
+
transition from: :run_agent, on: :child_completed, to: :done
|
|
162
|
+
transition from: :run_agent, on: :child_failed, to: :handle_error
|
|
163
|
+
```
|
|
164
|
+
|
|
105
165
|
### Multi-Agent — Agent-as-Tool pattern
|
|
106
166
|
|
|
107
167
|
Wrap sub-agents as `Tool::Base` subclasses so the orchestrator LLM can call them on demand.
|
|
@@ -116,6 +176,11 @@ class ResearchTool < Phronomy::Tool::Base
|
|
|
116
176
|
end
|
|
117
177
|
end
|
|
118
178
|
|
|
179
|
+
class WriterAgent < Phronomy::Agent::Base
|
|
180
|
+
model "gpt-4o"
|
|
181
|
+
instructions "You are a professional technical writer."
|
|
182
|
+
end
|
|
183
|
+
|
|
119
184
|
class WriteTool < Phronomy::Tool::Base
|
|
120
185
|
description "Write a technical blog post given research notes and a writing brief."
|
|
121
186
|
param :instructions, type: :string, desc: "Writing brief including research notes"
|
|
@@ -137,6 +202,9 @@ puts result[:output]
|
|
|
137
202
|
|
|
138
203
|
### Guardrails — Input/output validation
|
|
139
204
|
|
|
205
|
+
Call `fail!(reason)` inside `check` to reject — it raises `Phronomy::GuardrailError`.
|
|
206
|
+
When a guardrail rejects, `invoke` raises instead of returning an output.
|
|
207
|
+
|
|
140
208
|
```ruby
|
|
141
209
|
class NoSensitiveDataGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
142
210
|
def check(input)
|
|
@@ -146,8 +214,19 @@ end
|
|
|
146
214
|
|
|
147
215
|
agent = ResearchAgent.new
|
|
148
216
|
agent.add_input_guardrail(NoSensitiveDataGuardrail.new)
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
agent.invoke("Charge 4111-1111-1111-1111")
|
|
220
|
+
rescue Phronomy::GuardrailError => e
|
|
221
|
+
puts e.message # => "Credit card numbers are not allowed"
|
|
222
|
+
end
|
|
149
223
|
```
|
|
150
224
|
|
|
225
|
+
> **Limitations:** Phronomy ships no built-in guardrail implementations. There is no
|
|
226
|
+
> built-in prompt injection detector, PII scanner, or content classifier. All guardrail
|
|
227
|
+
> logic must be implemented by the application. Reference implementations for common
|
|
228
|
+
> patterns are available in `phronomy-examples` (example 06).
|
|
229
|
+
|
|
151
230
|
### Knowledge/RAG — Context injection and vector retrieval
|
|
152
231
|
|
|
153
232
|
```ruby
|
|
@@ -161,6 +240,13 @@ policy = Phronomy::KnowledgeSource::StaticKnowledge.new(
|
|
|
161
240
|
# RAG retrieval from a vector store
|
|
162
241
|
store = Phronomy::VectorStore::InMemory.new
|
|
163
242
|
embeddings = Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small")
|
|
243
|
+
|
|
244
|
+
# Add documents before querying
|
|
245
|
+
text1 = "Refunds are processed within 5 business days."
|
|
246
|
+
text2 = "Contact support@example.com for refund requests."
|
|
247
|
+
store.add(id: "doc-1", embedding: embeddings.embed(text1), metadata: { content: text1, source: "policy.md" })
|
|
248
|
+
store.add(id: "doc-2", embedding: embeddings.embed(text2), metadata: { content: text2, source: "policy.md" })
|
|
249
|
+
|
|
164
250
|
rag = Phronomy::KnowledgeSource::RAGKnowledge.new(store: store, embeddings: embeddings, k: 5)
|
|
165
251
|
|
|
166
252
|
# Inject at invocation time
|
|
@@ -168,6 +254,15 @@ result = MyAgent.new.invoke("What is the refund policy?",
|
|
|
168
254
|
config: { knowledge_sources: [policy, rag] })
|
|
169
255
|
```
|
|
170
256
|
|
|
257
|
+
`static_knowledge_refresh!` invalidates the class-level cache of *static* knowledge sources
|
|
258
|
+
(not RAG stores). Call it when the underlying file or content has changed:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# Static knowledge sources are cached at the class level after the first fetch.
|
|
262
|
+
# Call refresh! when the underlying content changes (e.g. after reloading policy.md).
|
|
263
|
+
MyAgent.static_knowledge_refresh!
|
|
264
|
+
```
|
|
265
|
+
|
|
171
266
|
Load and split documents with built-in loaders:
|
|
172
267
|
|
|
173
268
|
```ruby
|
|
@@ -211,7 +306,7 @@ Phronomy.configure do |c|
|
|
|
211
306
|
end
|
|
212
307
|
```
|
|
213
308
|
|
|
214
|
-
Hooks are called in order — global → class → instance — and
|
|
309
|
+
Hooks are called in order — global → class → instance — and shallow-merged (`Hash#merge`; last hook wins on key conflicts).
|
|
215
310
|
|
|
216
311
|
### GeneratorVerifier — Generator-Verifier loop with custom prompt builders
|
|
217
312
|
|
|
@@ -290,19 +385,21 @@ class MyOrchestrator < Phronomy::Agent::Orchestrator
|
|
|
290
385
|
instructions "Orchestrate."
|
|
291
386
|
|
|
292
387
|
def run(query)
|
|
293
|
-
# Heterogeneous agents in parallel (cap at 4 threads; skip failures)
|
|
388
|
+
# Heterogeneous agents in parallel (cap at 4 threads; skip failures; 30 s timeout)
|
|
294
389
|
results = dispatch_parallel(
|
|
295
390
|
{agent: SearchAgent, input: "topic A"},
|
|
296
391
|
{agent: AnalysisAgent, input: query},
|
|
297
392
|
max_concurrency: 4,
|
|
298
|
-
on_error: :skip
|
|
393
|
+
on_error: :skip,
|
|
394
|
+
timeout: 30
|
|
299
395
|
)
|
|
300
396
|
|
|
301
397
|
# Fan-out — same agent, multiple inputs
|
|
302
398
|
translations = fan_out(
|
|
303
399
|
agent: TranslationAgent,
|
|
304
400
|
inputs: %w[Hello World],
|
|
305
|
-
max_concurrency: 2
|
|
401
|
+
max_concurrency: 2,
|
|
402
|
+
timeout: 20
|
|
306
403
|
)
|
|
307
404
|
|
|
308
405
|
results.compact.map { |r| r[:output] }.join("\n")
|
|
@@ -324,13 +421,16 @@ end
|
|
|
324
421
|
app = Phronomy::Workflow.define(EnrichContext) do
|
|
325
422
|
initial :enrich
|
|
326
423
|
state :enrich, action: ->(s) do
|
|
327
|
-
results
|
|
328
|
-
threads =
|
|
329
|
-
Thread.new {
|
|
330
|
-
Thread.new {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
424
|
+
# Use Thread#value to collect results safely — avoids concurrent Hash writes
|
|
425
|
+
threads = {
|
|
426
|
+
summary: Thread.new { Summarizer.call(s) },
|
|
427
|
+
tags: Thread.new { Tagger.call(s) }
|
|
428
|
+
}
|
|
429
|
+
# For production use, wrap with Timeout.timeout to avoid unbounded waits:
|
|
430
|
+
# require "timeout"
|
|
431
|
+
# Timeout.timeout(30) { threads.each_value(&:join) }
|
|
432
|
+
threads.each_value(&:join)
|
|
433
|
+
s.merge(summary: threads[:summary].value, tags: Array(threads[:tags].value))
|
|
334
434
|
end
|
|
335
435
|
transition from: :enrich, to: :__finish__
|
|
336
436
|
end
|
|
@@ -419,22 +519,38 @@ puts result2[:output] # => "Your name is Alice."
|
|
|
419
519
|
`result[:messages]` contains the complete message history after each invocation.
|
|
420
520
|
Persist it however suits your application (in-memory hash, Redis, ActiveRecord, etc.).
|
|
421
521
|
|
|
522
|
+
> **Note on `thread_id`**: `thread_id` is a correlation identifier used internally for
|
|
523
|
+
> checkpoint/compaction context and EventLoop routing. It does **not** automatically persist or
|
|
524
|
+
> restore conversation history — you must pass `messages:` explicitly on each turn as shown above.
|
|
525
|
+
|
|
422
526
|
|
|
423
527
|
## Configuration
|
|
424
528
|
|
|
425
529
|
```ruby
|
|
426
530
|
Phronomy.configure do |c|
|
|
427
|
-
c.default_model
|
|
428
|
-
c.recursion_limit
|
|
429
|
-
c.tracer
|
|
430
|
-
c.before_completion
|
|
531
|
+
c.default_model = "gpt-4o-mini"
|
|
532
|
+
c.recursion_limit = 25
|
|
533
|
+
c.tracer = Phronomy::Tracing::NullTracer.new
|
|
534
|
+
c.before_completion = nil # optional; global hook lambda
|
|
535
|
+
c.trace_pii = false # default; set to true only when trace data contains no PII
|
|
536
|
+
c.logger = nil # optional; any object responding to #warn (e.g. Rails.logger)
|
|
537
|
+
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on EventLoop#stop(drain: true)
|
|
431
538
|
end
|
|
432
539
|
```
|
|
433
540
|
|
|
541
|
+
`c.logger` receives framework diagnostic messages (e.g. unreachable-state warnings from
|
|
542
|
+
`Workflow.define`). When `nil` (default), messages are written to `$stderr` via `Kernel#warn`.
|
|
543
|
+
|
|
544
|
+
> **Note**: When `trace_pii = false`, both the _input_ and the _output_ (LLM
|
|
545
|
+
> responses and tool results) are replaced with `[REDACTED]` in trace spans.
|
|
546
|
+
> The default is `false` (PII protection enabled). Set to `true` only when
|
|
547
|
+
> trace data does not contain sensitive information.
|
|
548
|
+
|
|
434
549
|
## Context Management
|
|
435
550
|
|
|
436
|
-
Phronomy includes a context window management layer
|
|
437
|
-
|
|
551
|
+
Phronomy includes a context window management layer. When model metadata is
|
|
552
|
+
available (either from the built-in registry or via an explicit `context_window:` setting),
|
|
553
|
+
agents automatically stay within the configured token limit.
|
|
438
554
|
|
|
439
555
|
### TokenBudget
|
|
440
556
|
|
|
@@ -466,12 +582,82 @@ class MyAgent < Phronomy::Agent::Base
|
|
|
466
582
|
model "gpt-4o"
|
|
467
583
|
max_output_tokens 4096 # override max_output_tokens from registry
|
|
468
584
|
context_overhead 600 # extra reservation for system prompt + tools
|
|
585
|
+
invoke_timeout 30 # raise Phronomy::TimeoutError after 30 s (wait timeout, not cancellation)
|
|
586
|
+
max_parallel_tools 4 # cap concurrent tool-call threads (default: 10)
|
|
469
587
|
end
|
|
470
588
|
```
|
|
471
589
|
|
|
472
590
|
`Agent::Base#invoke` builds a `TokenBudget` automatically. When the model is not in the
|
|
473
591
|
registry the budget is silently skipped.
|
|
474
592
|
|
|
593
|
+
> **Note on CJK languages**: The default `TokenEstimator` uses a character-ratio heuristic
|
|
594
|
+
> calibrated for ASCII/Latin text (4 chars/token). For Chinese, Japanese, and Korean text,
|
|
595
|
+
> actual token counts are approximately **4× higher** than the estimate because CJK
|
|
596
|
+
> characters are typically 1 token each. For accurate CJK token counting, supply a
|
|
597
|
+
> tokenizer-backed callable:
|
|
598
|
+
>
|
|
599
|
+
> ```ruby
|
|
600
|
+
> require "tiktoken_ruby"
|
|
601
|
+
> enc = Tiktoken.encoding_for_model("gpt-4o")
|
|
602
|
+
> Phronomy::Context::TokenEstimator.tokenizer = ->(text) { enc.encode(text).length }
|
|
603
|
+
> ```
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
### CancellationToken — Cooperative cancellation
|
|
607
|
+
|
|
608
|
+
Pass a `CancellationToken` to any agent via `config: { cancellation_token: token }`.
|
|
609
|
+
Cancellation is checked at multiple granular checkpoints: before the LLM call, before
|
|
610
|
+
each RAG knowledge-source fetch, after each streaming chunk, before each parallel
|
|
611
|
+
tool-call batch, and after each `before_completion` hook. `CancellationError` is
|
|
612
|
+
raised immediately and is never retried. No threads are force-killed — `ensure`
|
|
613
|
+
blocks always execute.
|
|
614
|
+
|
|
615
|
+
> **Cooperative cancellation — not preemptive**
|
|
616
|
+
>
|
|
617
|
+
> Phronomy uses _cooperative boundary cancellation_. The token is polled at the
|
|
618
|
+
> checkpoints listed above; it is **not** injected as a signal into a running
|
|
619
|
+
> operation. This means the following are **not** interrupted mid-execution:
|
|
620
|
+
>
|
|
621
|
+
> - A single `KnowledgeSource#fetch` that is already blocking (e.g. HTTP call)
|
|
622
|
+
> - A single `chat.ask` call that is not streaming
|
|
623
|
+
> - A single `tool.execute` call that is already running
|
|
624
|
+
> - Any external I/O (database query, vector search, HTTP request) inside those calls
|
|
625
|
+
>
|
|
626
|
+
> For deep in-flight safety, complement `CancellationToken` with per-source or
|
|
627
|
+
> per-tool timeouts (e.g. `Net::HTTP#read_timeout`, `Timeout.timeout`, connection
|
|
628
|
+
> pool limits). Ruby's GVL prevents fully preemptive cancellation without
|
|
629
|
+
> `Thread#kill`, which Phronomy avoids by default due to resource safety concerns.
|
|
630
|
+
|
|
631
|
+
```ruby
|
|
632
|
+
token = Phronomy::CancellationToken.new
|
|
633
|
+
|
|
634
|
+
# Cancel from another thread after 5 s
|
|
635
|
+
Thread.new { sleep 5; token.cancel! }
|
|
636
|
+
|
|
637
|
+
begin
|
|
638
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
639
|
+
rescue Phronomy::CancellationError
|
|
640
|
+
puts "cancelled"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Hard deadline via monotonic clock (recommended — immune to NTP/DST changes)
|
|
644
|
+
token = Phronomy::CancellationToken.timeout_after(30)
|
|
645
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
646
|
+
|
|
647
|
+
# Hard deadline via wall-clock (legacy — still supported)
|
|
648
|
+
token = Phronomy::CancellationToken.new(deadline: Time.now + 30)
|
|
649
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
650
|
+
|
|
651
|
+
# Propagate to all parallel workers via dispatch_parallel / fan_out
|
|
652
|
+
token = Phronomy::CancellationToken.new
|
|
653
|
+
Thread.new { sleep 10; token.cancel! }
|
|
654
|
+
|
|
655
|
+
orchestrator.dispatch_parallel(
|
|
656
|
+
{agent: SearchAgent, input: "topic A"},
|
|
657
|
+
{agent: AnalysisAgent, input: "topic B"},
|
|
658
|
+
cancellation_token: token
|
|
659
|
+
)
|
|
660
|
+
```
|
|
475
661
|
|
|
476
662
|
## Examples
|
|
477
663
|
|
|
@@ -542,6 +728,35 @@ bin/console
|
|
|
542
728
|
|
|
543
729
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Raizo-TCS/phronomy.
|
|
544
730
|
|
|
731
|
+
## Security & Privacy
|
|
732
|
+
|
|
733
|
+
**API credentials** — Phronomy does not store or transmit your LLM API keys. All
|
|
734
|
+
credentials are handled by RubyLLM and passed directly to the provider.
|
|
735
|
+
|
|
736
|
+
**Tracing and PII** — When tracing is enabled (`Phronomy::Tracing::OpenTelemetryTracer`
|
|
737
|
+
or a custom tracer), agent inputs and LLM outputs are replaced with `[REDACTED]` in
|
|
738
|
+
span attributes by default (`trace_pii: false`). To include full content in traces
|
|
739
|
+
(e.g., for debugging in a non-production environment), set `trace_pii: true` in your
|
|
740
|
+
Phronomy configuration. Evaluate whether your tracing backend (OTLP collector, Jaeger,
|
|
741
|
+
Honeycomb, etc.) meets your data-retention and privacy requirements.
|
|
742
|
+
|
|
743
|
+
**Prompt injection** — Phronomy provides no built-in prompt injection detection.
|
|
744
|
+
Applications that process untrusted user input should implement their own input
|
|
745
|
+
guardrails (see the Guardrails section above).
|
|
746
|
+
|
|
747
|
+
**Tool and MCP security** — Tools can perform real-world side effects (database
|
|
748
|
+
writes, API calls, file deletion). Treat tool execution as a privileged operation:
|
|
749
|
+
use the interrupt/approval mechanism for high-risk tools (e.g., payment processing,
|
|
750
|
+
file deletion) rather than allowing fully autonomous execution. MCP servers are
|
|
751
|
+
external trust boundaries: connect only to servers you control. A compromised MCP
|
|
752
|
+
server can inject instructions that manipulate agent behavior (tool-level prompt
|
|
753
|
+
injection). Avoid passing secrets as direct tool parameters — if `trace_pii: true`
|
|
754
|
+
is set, tool arguments are captured in trace spans.
|
|
755
|
+
|
|
756
|
+
**Vulnerability reports** — Please report security vulnerabilities privately via
|
|
757
|
+
GitHub's [Security Advisories](https://github.com/Raizo-TCS/phronomy/security/advisories)
|
|
758
|
+
rather than opening a public issue.
|
|
759
|
+
|
|
545
760
|
## License
|
|
546
761
|
|
|
547
762
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Release Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist before every release of the `phronomy` gem.
|
|
4
|
+
Copy it into the GitHub Release draft and check off each item.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Pre-release
|
|
9
|
+
|
|
10
|
+
- [ ] `CHANGELOG.md` updated (Added / Changed / Fixed / Removed / Deprecated / Security)
|
|
11
|
+
- [ ] Version bumped in `lib/phronomy/version.rb`
|
|
12
|
+
- [ ] Stability table in `README.md` reflects any API additions, removals, or promotions
|
|
13
|
+
- [ ] `@api private` annotations are consistent with the README stability table (Issue #205)
|
|
14
|
+
- [ ] Public API compatibility snapshot regenerated if any Stable API changed:
|
|
15
|
+
```bash
|
|
16
|
+
bundle exec ruby scripts/api_snapshot.rb --write
|
|
17
|
+
```
|
|
18
|
+
(Issue #210)
|
|
19
|
+
- [ ] Migration notes or deprecation warnings added for any breaking changes
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quality Gates (all must pass before tagging)
|
|
24
|
+
|
|
25
|
+
- [ ] `bundle exec rspec --format documentation` — 0 failures
|
|
26
|
+
- [ ] `bundle exec rspec --tag integration` — 0 failures, all expected pending
|
|
27
|
+
- [ ] `ruby scripts/check_japanese.rb` — exit 0 (no Japanese in source)
|
|
28
|
+
- [ ] `bundle exec standardrb` — 0 offenses
|
|
29
|
+
- [ ] `COVERAGE=1 bundle exec rspec` — coverage above configured threshold (Issue #207)
|
|
30
|
+
- [ ] CI green on all Ruby matrix versions (3.2 / 3.3 / 3.4 / head)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Security Review
|
|
35
|
+
|
|
36
|
+
- [ ] `SECURITY.md` is up to date (supported versions table, contact info)
|
|
37
|
+
- [ ] No new `trace_pii`-sensitive data paths introduced without redaction
|
|
38
|
+
- [ ] No new `requires_approval` tools missing the approval gate
|
|
39
|
+
- [ ] No secrets, credentials, or PII in tool descriptions, schema strings, or spec fixtures
|
|
40
|
+
- [ ] Dependency audit passes: `bundle exec bundler-audit check --update`
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Release Steps
|
|
45
|
+
|
|
46
|
+
> **Do not use `gem push` directly.** The GitHub Actions release workflow handles
|
|
47
|
+
> gem publication. Follow the steps below exactly.
|
|
48
|
+
|
|
49
|
+
1. Commit the version bump:
|
|
50
|
+
```bash
|
|
51
|
+
git commit -m "bump version to X.Y.Z"
|
|
52
|
+
git push origin main
|
|
53
|
+
```
|
|
54
|
+
2. Create and push the tag:
|
|
55
|
+
```bash
|
|
56
|
+
git tag vX.Y.Z
|
|
57
|
+
git push origin vX.Y.Z
|
|
58
|
+
```
|
|
59
|
+
3. Trigger the release workflow:
|
|
60
|
+
```bash
|
|
61
|
+
gh workflow run release.yml --field tag=vX.Y.Z
|
|
62
|
+
```
|
|
63
|
+
4. Monitor the workflow run:
|
|
64
|
+
```bash
|
|
65
|
+
gh run list --workflow release.yml --limit 3
|
|
66
|
+
```
|
|
67
|
+
5. Verify the gem appears on RubyGems: `gem search phronomy`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Post-release
|
|
72
|
+
|
|
73
|
+
- [ ] `phronomy-examples` `Gemfile` updated to the new version
|
|
74
|
+
```bash
|
|
75
|
+
cd ../phronomy-examples && bundle update phronomy
|
|
76
|
+
```
|
|
77
|
+
- [ ] `phronomy-examples` tests pass after the update
|
|
78
|
+
- [ ] GitHub Release description includes the relevant CHANGELOG excerpt
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Reference Issues
|
|
83
|
+
|
|
84
|
+
- #205 — `@api private` annotation policy
|
|
85
|
+
- #207 — SimpleCov coverage gate
|
|
86
|
+
- #210 — Public API compatibility snapshot
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| Latest release (main branch) | ✅ |
|
|
8
|
+
| Older versions | ❌ — please upgrade |
|
|
9
|
+
|
|
10
|
+
Only the latest released version of `phronomy` receives security patches. If you
|
|
11
|
+
are running an older version, please upgrade before filing a report.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
**Please do NOT open a public GitHub Issue for security vulnerabilities.**
|
|
18
|
+
|
|
19
|
+
Use [GitHub's private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
|
|
20
|
+
instead:
|
|
21
|
+
|
|
22
|
+
1. Navigate to the [Security tab](https://github.com/Raizo-TCS/phronomy/security)
|
|
23
|
+
of this repository.
|
|
24
|
+
2. Click **"Report a vulnerability"**.
|
|
25
|
+
3. Fill in the advisory form with as much detail as possible.
|
|
26
|
+
|
|
27
|
+
This creates a private draft advisory visible only to maintainers, keeping the
|
|
28
|
+
details confidential until a fix is prepared and released.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Response Timeline
|
|
33
|
+
|
|
34
|
+
| Milestone | Target |
|
|
35
|
+
|-----------|--------|
|
|
36
|
+
| Acknowledgement of report | Within **7 days** |
|
|
37
|
+
| Triage and initial assessment | Within **14 days** |
|
|
38
|
+
| Patch release (critical / high severity) | Within **30 days** |
|
|
39
|
+
| Patch release (medium / low severity) | Best effort; typically within **60 days** |
|
|
40
|
+
|
|
41
|
+
If you do not receive an acknowledgement within 7 days, please follow up by
|
|
42
|
+
opening a **public** Issue with the subject "Security report follow-up (no
|
|
43
|
+
response)" — do **not** include vulnerability details in the public Issue.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Scope
|
|
48
|
+
|
|
49
|
+
**In scope:**
|
|
50
|
+
|
|
51
|
+
- Vulnerabilities in the `phronomy` gem source code (`lib/`, `spec/`).
|
|
52
|
+
- Dependency vulnerabilities that affect gem consumers when `phronomy` is used as intended.
|
|
53
|
+
- Information disclosure via tracing/logging APIs (e.g. `trace_pii: false` bypass).
|
|
54
|
+
- Approval gate bypasses (tool execution without the registered approval handler).
|
|
55
|
+
|
|
56
|
+
**Out of scope:**
|
|
57
|
+
|
|
58
|
+
- Security of consumer applications built on top of `phronomy`.
|
|
59
|
+
- Vulnerabilities in the LLM provider (OpenAI, Anthropic, etc.) or in `ruby_llm`.
|
|
60
|
+
- Attacks that require an attacker to already have write access to the host system.
|
|
61
|
+
- Prompt injection via LLM output — the gem forwards LLM output faithfully; prompt
|
|
62
|
+
injection resistance is the responsibility of the LLM provider and the application.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Disclosure Policy
|
|
67
|
+
|
|
68
|
+
- Maintainers will coordinate with you on the release date and credit you in the
|
|
69
|
+
`CHANGELOG.md` entry and GitHub release notes.
|
|
70
|
+
- If you wish to remain anonymous, let us know in the advisory.
|
|
71
|
+
- We follow a **coordinated disclosure** model: the advisory will be made public
|
|
72
|
+
after a patch is released (or after 90 days, whichever comes first).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Credit
|
|
77
|
+
|
|
78
|
+
Security reporters are credited in the `CHANGELOG.md` entry for the patch release,
|
|
79
|
+
in the GitHub Security Advisory, and in the release notes — unless they request
|
|
80
|
+
anonymity.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workflow_context_merge": 124364.81010472385,
|
|
3
|
+
"workflow_define": 2179.945274115319,
|
|
4
|
+
"tool_params_schema_definition": 19534379.159046534,
|
|
5
|
+
"dispatch_parallel_10": 1483.2255243486482,
|
|
6
|
+
"cancellation_token_cancelled": 4335060.97443425,
|
|
7
|
+
"cancellation_token_raise_if_cancelled_noop": 3566903.189098373,
|
|
8
|
+
"trim_context_remove_2000": 1761.5700678986254
|
|
9
|
+
}
|