phronomy 0.6.0 → 0.7.1
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 +22 -0
- data/CHANGELOG.md +488 -0
- data/CONTRIBUTING.md +102 -0
- data/README.md +374 -36
- data/RELEASE_CHECKLIST.md +86 -0
- data/Rakefile +33 -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 +172 -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 +75 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +45 -0
- data/docs/decisions/006-no-built-in-guardrails.md +66 -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/docs/decisions/010-cooperative-first-concurrency.md +248 -0
- data/lib/phronomy/agent/base.rb +416 -49
- 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 +19 -0
- data/lib/phronomy/agent/fsm.rb +44 -52
- data/lib/phronomy/agent/handoff.rb +3 -0
- data/lib/phronomy/agent/orchestrator.rb +191 -54
- data/lib/phronomy/agent/parallel_tool_chat.rb +87 -13
- data/lib/phronomy/agent/react_agent.rb +16 -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/async_queue.rb +155 -0
- data/lib/phronomy/blocking_adapter_pool.rb +435 -0
- data/lib/phronomy/cancellation_scope.rb +123 -0
- data/lib/phronomy/cancellation_token.rb +133 -0
- data/lib/phronomy/concurrency_gate.rb +155 -0
- data/lib/phronomy/configuration.rb +168 -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/deadline.rb +63 -0
- data/lib/phronomy/diagnostics.rb +62 -0
- data/lib/phronomy/embeddings/base.rb +22 -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 +11 -9
- 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 +275 -30
- data/lib/phronomy/fsm_session.rb +57 -4
- data/lib/phronomy/generator_verifier.rb +2 -0
- data/lib/phronomy/guardrail/base.rb +3 -0
- data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
- data/lib/phronomy/invocation_context.rb +152 -0
- data/lib/phronomy/knowledge_source/base.rb +24 -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/llm_adapter/base.rb +104 -0
- data/lib/phronomy/llm_adapter/ruby_llm.rb +41 -0
- data/lib/phronomy/llm_adapter.rb +20 -0
- 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/metrics.rb +38 -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/runtime/deterministic_scheduler.rb +412 -0
- data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
- data/lib/phronomy/runtime/gate_registry.rb +52 -0
- data/lib/phronomy/runtime/pool_registry.rb +57 -0
- data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
- data/lib/phronomy/runtime/scheduler.rb +98 -0
- data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
- data/lib/phronomy/runtime/task_registry.rb +48 -0
- data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
- data/lib/phronomy/runtime/timer_queue.rb +106 -0
- data/lib/phronomy/runtime/timer_service.rb +42 -0
- data/lib/phronomy/runtime.rb +374 -0
- 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/task/backend.rb +80 -0
- data/lib/phronomy/task/fiber_backend.rb +157 -0
- data/lib/phronomy/task/immediate_backend.rb +89 -0
- data/lib/phronomy/task/thread_backend.rb +84 -0
- data/lib/phronomy/task.rb +275 -0
- data/lib/phronomy/task_group.rb +265 -0
- data/lib/phronomy/testing/fake_clock.rb +109 -0
- data/lib/phronomy/testing/fake_scheduler.rb +104 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
- data/lib/phronomy/testing.rb +12 -0
- data/lib/phronomy/tool/agent_tool.rb +1 -0
- data/lib/phronomy/tool/base.rb +298 -28
- data/lib/phronomy/tool/mcp_tool.rb +103 -17
- data/lib/phronomy/tool/scope_policy.rb +50 -0
- data/lib/phronomy/tool_executor.rb +106 -0
- 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 +36 -0
- data/lib/phronomy/vector_store/async_backend.rb +110 -0
- data/lib/phronomy/vector_store/base.rb +40 -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 +147 -11
- data/lib/phronomy/workflow_context.rb +83 -6
- data/lib/phronomy/workflow_runner.rb +106 -7
- data/lib/phronomy.rb +112 -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 +83 -2
data/README.md
CHANGED
|
@@ -1,35 +1,102 @@
|
|
|
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.
|
|
24
|
+
|
|
25
|
+
**Core building blocks**
|
|
11
26
|
|
|
12
27
|
| Feature | Stability |
|
|
13
28
|
|---|---|
|
|
14
29
|
| **Workflow** — Stateful, branching workflows with wait_state/send_event | Stable |
|
|
15
|
-
| **Workflow
|
|
16
|
-
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#run_as_child` (child-FSM pattern for Workflow integration), parallel tool dispatch via `ParallelToolChat` | Experimental |
|
|
17
|
-
| **Workflow Parallel Node** — Concurrent branches via application-level threads | Beta |
|
|
30
|
+
| **Workflow action_timeout** — Per-state `action_timeout:` keyword on `state` DSL; cancels Task-returning entry actions that exceed the limit and raises `Phronomy::ActionTimeoutError` | Beta |
|
|
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 |
|
|
22
|
-
| **Multi-agent** — Agent-as-Tool pattern and hub-and-spoke handoff routing | Beta |
|
|
23
|
-
| **GeneratorVerifier** — Generator-Verifier loop with injectable prompt builders/parsers | Beta |
|
|
24
|
-
| **Agent::Orchestrator** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
25
|
-
| **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + stateful worker pool with task queue (worker-local message history per run) | Beta |
|
|
26
|
-
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
27
34
|
| **Guardrails** — Input/output validation with custom `InputGuardrail`/`OutputGuardrail` | Beta |
|
|
35
|
+
| **`PromptInjectionGuardrail`** — Built-in `InputGuardrail` subclass that detects prompt-injection patterns; usable standalone or as part of a guardrail chain | Beta |
|
|
36
|
+
| **`Tool::Base.redact_params` / `.max_result_size`** — Class-level DSL: `redact_params` masks parameter values in log/trace output; `max_result_size` truncates oversized tool results before they reach the LLM | Beta |
|
|
28
37
|
| **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
|
|
29
38
|
| **Eval Framework** — Dataset-driven evaluation with multiple scorer types | Beta |
|
|
30
39
|
| **Tracing** — Pluggable span-based observability | Stable |
|
|
40
|
+
| **Error Taxonomy** — `RateLimitError`, `AuthenticationError`, `ContextLengthError`, `TransportError` (subclasses of `Phronomy::Error`) raised at the agent retry boundary | Beta |
|
|
41
|
+
|
|
42
|
+
**Knowledge and integration**
|
|
43
|
+
|
|
44
|
+
| Feature | Stability |
|
|
45
|
+
|---|---|
|
|
46
|
+
| **Knowledge/RAG** — Retrieval sources with pluggable loaders, splitters, and vector stores; `static_knowledge_refresh!` for runtime cache invalidation | Beta |
|
|
47
|
+
| **`VectorStore#size`** — Returns document count for all three backends (InMemory, RedisSearch, Pgvector) | Beta |
|
|
48
|
+
| **`VectorStore::AsyncBackend` mixin** — Pluggable async interface for `VectorStore`; default pool-backed implementations for `search_async`, `add_async`, `remove_async`, `clear_async`; backends with native async drivers override individual methods to bypass `BlockingAdapterPool` entirely; all existing backends remain unchanged | Beta |
|
|
49
|
+
| **Parallel RAG multi-source fetch** — `Agent#build_context` fetches all `knowledge_sources` concurrently via `TaskGroup`; `config[:rag_failure_policy]` `:skip` (default) silently ignores failed sources so the agent answers with partial context, `:fail` surfaces the first error; per-source latency is emitted to `Phronomy.configuration.logger` at debug level | Beta |
|
|
31
50
|
| **MCP Tool** — Model Context Protocol server integration | Beta |
|
|
32
51
|
|
|
52
|
+
**Execution and reliability**
|
|
53
|
+
|
|
54
|
+
| Feature | Stability |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **Workflow EventLoop Mode** — Opt-in event-driven execution: `Phronomy.configure { \|c\| c.event_loop = true }` | Experimental |
|
|
57
|
+
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#run_as_child` (child-FSM pattern for Workflow integration), parallel tool dispatch via `ParallelToolChat` | Experimental |
|
|
58
|
+
| **`invoke_async` / `call_async`** — `Agent::Base#invoke_async` and `Workflow#invoke_async` return a `Task`; `Tool::Base#call_async` similarly; compatible with EventLoop and standalone contexts | Experimental |
|
|
59
|
+
| **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 |
|
|
60
|
+
| **`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 |
|
|
61
|
+
| **`execution_mode` DSL on `Tool::Base`** — Declares how a tool's `execute` should be dispatched: `:cooperative` (same scheduler thread), `:blocking_io` (default; offloaded to `BlockingAdapterPool`), `:cpu_bound`, `:external_process` | Experimental |
|
|
62
|
+
| **`invocation_context:` keyword on `Agent#invoke` / `Workflow#invoke`** — Pass a `Phronomy::InvocationContext` directly; `thread_id`, `cancellation_token`, and `deadline`-based timeout are derived from it; `task_id` / `parent_task_id` appear in trace spans automatically; `config:` keys remain supported as backward-compat aliases | Beta |
|
|
63
|
+
| **`ConcurrencyGate` — unified backpressure** — Counting semaphore that enforces per-resource concurrency caps (`max_concurrent_agent_tasks`, `max_concurrent_tool_tasks`, `max_concurrent_workflow_tasks`, `max_concurrent_llm_calls`, `max_concurrent_rag_fetches`, `max_concurrent_vector_searches`); configured via `Phronomy.configure`; backpressure behaviour follows the global `backpressure` setting (`:wait`, `:raise`/`:reject`, `:timeout`); `nil` cap = unlimited (default) | Beta |
|
|
64
|
+
| **Cooperative scheduler yield points** — `Runtime#yield` (cooperative yield; yields the current task's time slice); `Runtime#yield_if_needed(every: N)` (thread-local counter, yields every N calls); CPU-bound detection when `blocking_detect_threshold_ms` is set (warns and increments `non_yield_threshold_violation_count` when a task runs longer than the threshold without yielding); `starvation_threshold_ms` configuration field (default: 50ms) | Beta |
|
|
65
|
+
| **`Phronomy::Metrics`** — `Phronomy::Metrics.snapshot` returns task-tree and pool counters; task-centric keys: `active_agent_tasks`, `active_tool_tasks`, `active_workflow_tasks`, `active_rag_tasks`, `active_llm_tasks`, `task_wait_time_p50_ms`, `task_wait_time_p95_ms`, `task_run_time_p50_ms`, `task_run_time_p95_ms`, `cancelled_tasks`, `failed_tasks`, `non_yield_threshold_violation_count`; pool/event-loop keys remain for backward compatibility; `Runtime#task_snapshot` exposes task-centric metrics directly | Beta |
|
|
66
|
+
| **`Phronomy.with_configuration` / `Phronomy.reset_runtime!`** — Scoped configuration override and full runtime reset for test isolation | Beta |
|
|
67
|
+
|
|
68
|
+
**Agent patterns**
|
|
69
|
+
|
|
70
|
+
| Feature | Stability |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **Workflow parallel pattern** — Concurrent branches via application-level threads (no built-in parallel primitive; see the Workflow section for the recommended pattern) | Beta |
|
|
73
|
+
| **Multi-agent** — Agent-as-Tool pattern and hub-and-spoke handoff routing | Beta |
|
|
74
|
+
| **GeneratorVerifier** — Generator-Verifier loop with injectable prompt builders/parsers | Beta |
|
|
75
|
+
| **Agent::Orchestrator** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
|
|
76
|
+
| **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + stateful workers with sequential task assignment (worker-local message history persisted across tasks) | Beta |
|
|
77
|
+
| **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
|
|
78
|
+
| **`ScopePolicy`** — Configurable policy callable that maps (tool, scope, agent) to `:allow`/`:approve`/`:reject`; default policy auto-routes high-risk scopes through the approval gate | Experimental |
|
|
79
|
+
|
|
80
|
+
> **Public API boundary**: The tables above are the complete list of classes, modules, and features
|
|
81
|
+
> intended for gem consumers. Every entry has an associated stability label.
|
|
82
|
+
> All other classes, modules, and methods — including everything in the
|
|
83
|
+
> [Advanced / Internal APIs](#advanced--internal-apis) section below — are
|
|
84
|
+
> marked `@api private` in source and may change without notice. Do not
|
|
85
|
+
> depend on internal APIs in application code.
|
|
86
|
+
|
|
87
|
+
## Advanced / Internal APIs
|
|
88
|
+
|
|
89
|
+
The APIs listed below are intended for advanced use cases, framework internals, and test infrastructure. Typical application code does not need to interact with them directly.
|
|
90
|
+
|
|
91
|
+
> These APIs are subject to change without the same backwards-compatibility guarantees as the stable public API.
|
|
92
|
+
|
|
93
|
+
| Feature | Stability |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **`Phronomy::Diagnostics`** — Snapshot of scheduler internals for debug/monitoring; `SchedulerReentrancyError` raised on invalid re-entrant scheduler use; `Runtime.in_scheduler_context?` returns `true` when called from inside a scheduler task | Experimental |
|
|
96
|
+
| **`Phronomy::Testing::FakeClock` / `FakeScheduler` / `SchedulerHelpers`** — Test helpers for deterministic concurrency specs: `FakeClock#advance(seconds)` controls time; `FakeScheduler` runs tasks synchronously and records `event_log`; `FakeScheduler#assert_order` / `#assert_cancelled` for ordering assertions; `FakeClock#advance_to_next_timer` fires the next pending callback; `Testing::SchedulerHelpers#with_fake_scheduler` replaces the global Runtime for the duration of a block | Beta |
|
|
97
|
+
| **`Configuration#runtime_backend`** — `:thread` (default, one OS thread per task), `:immediate` (tests — tasks run synchronously, no extra threads), `:fiber` (**EXPERIMENTAL** — experimental validation backend only: runs tasks as Ruby Fibers on a cooperative scheduler to verify that framework components are truly non-blocking; **not for production use** and not a planned production replacement for `:thread`; no preemptive scheduling will be added). `:cooperative` is a **deprecated alias** for `:immediate` — do not use in new code | Beta |
|
|
98
|
+
| **`Configuration#strict_runtime_guards`** — When `true`, calling `Agent#invoke` from inside a scheduler task raises `SchedulerReentrancyError`; when `false` (default) a warning is logged instead | Beta |
|
|
99
|
+
|
|
33
100
|
## Installation
|
|
34
101
|
|
|
35
102
|
Add to your Gemfile:
|
|
@@ -44,17 +111,42 @@ Then run:
|
|
|
44
111
|
bundle install
|
|
45
112
|
```
|
|
46
113
|
|
|
114
|
+
### RubyLLM setup
|
|
115
|
+
|
|
116
|
+
Phronomy uses [RubyLLM](https://github.com/crmne/ruby_llm) for LLM access.
|
|
117
|
+
Configure your provider credentials before using agents or chains:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
RubyLLM.configure do |c|
|
|
121
|
+
c.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
122
|
+
# c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See the [RubyLLM documentation](https://rubyllm.com) for all supported providers.
|
|
127
|
+
|
|
128
|
+
### Optional dependencies
|
|
129
|
+
|
|
130
|
+
Install additional gems only for the features you use:
|
|
131
|
+
|
|
132
|
+
| Gem | Required for |
|
|
133
|
+
|-----|-------------|
|
|
134
|
+
| `pgvector` | `Phronomy::VectorStore::Pgvector` |
|
|
135
|
+
| `redis` | `Phronomy::VectorStore::RedisSearch` |
|
|
136
|
+
| `opentelemetry-api` | `Phronomy::Tracing::OpenTelemetryTracer` |
|
|
137
|
+
|
|
47
138
|
## Quick Start
|
|
48
139
|
|
|
49
140
|
### Agent — ReAct tool-calling agent
|
|
50
141
|
|
|
51
|
-
```ruby
|
|
142
|
+
```ruby runnable
|
|
52
143
|
class WebSearch < Phronomy::Tool::Base
|
|
53
144
|
description "Search the web"
|
|
54
145
|
param :query, type: :string, desc: "Search query"
|
|
55
146
|
|
|
56
147
|
def execute(query:)
|
|
57
|
-
#
|
|
148
|
+
# Replace with a real search API call (e.g., SerpAPI, Tavily)
|
|
149
|
+
"Mock search result for: #{query}"
|
|
58
150
|
end
|
|
59
151
|
end
|
|
60
152
|
|
|
@@ -71,7 +163,7 @@ puts result[:output]
|
|
|
71
163
|
|
|
72
164
|
### Workflow — Stateful workflow with wait_state/send_event
|
|
73
165
|
|
|
74
|
-
```ruby
|
|
166
|
+
```ruby runnable
|
|
75
167
|
class ReviewContext
|
|
76
168
|
include Phronomy::WorkflowContext
|
|
77
169
|
field :draft, type: :replace
|
|
@@ -79,10 +171,14 @@ class ReviewContext
|
|
|
79
171
|
field :approved, type: :replace, default: false
|
|
80
172
|
end
|
|
81
173
|
|
|
174
|
+
# Placeholder callables representing your own implementation
|
|
175
|
+
write_draft = ->(state) { state.merge(draft: "Draft content here") }
|
|
176
|
+
review_draft = ->(state) { state.merge(feedback: "Feedback on: #{state.draft}") }
|
|
177
|
+
|
|
82
178
|
app = Phronomy::Workflow.define(ReviewContext) do
|
|
83
179
|
initial :write
|
|
84
|
-
state :write, action:
|
|
85
|
-
state :review, action:
|
|
180
|
+
state :write, action: write_draft
|
|
181
|
+
state :review, action: review_draft
|
|
86
182
|
wait_state :awaiting_approval # halts here for human decision
|
|
87
183
|
state :finalize, action: ->(s) { s.merge(approved: true) }
|
|
88
184
|
transition from: :write, to: :review
|
|
@@ -102,6 +198,22 @@ final = app.send_event(state: state, event: :approve)
|
|
|
102
198
|
puts "Approved: #{final.approved}" # => true
|
|
103
199
|
```
|
|
104
200
|
|
|
201
|
+
In EventLoop mode (`c.event_loop = true`), `Agent#run_as_child` spawns a child agent
|
|
202
|
+
asynchronously. When the child succeeds, `:child_completed` is dispatched with the result
|
|
203
|
+
`{ output:, messages:, usage: }` as its payload; when it fails, `:child_failed` is
|
|
204
|
+
dispatched. Always declare both transitions to avoid a stuck workflow:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# EventLoop mode: workflow that runs an agent as a child FSM.
|
|
208
|
+
# The result { output:, messages:, usage: } arrives as the :child_completed event
|
|
209
|
+
# payload — write it back to the context in the target state's entry action.
|
|
210
|
+
entry :run_agent, ->(ctx) {
|
|
211
|
+
MyAgent.new.run_as_child(ctx.query, ctx: ctx)
|
|
212
|
+
}
|
|
213
|
+
transition from: :run_agent, on: :child_completed, to: :done
|
|
214
|
+
transition from: :run_agent, on: :child_failed, to: :handle_error
|
|
215
|
+
```
|
|
216
|
+
|
|
105
217
|
### Multi-Agent — Agent-as-Tool pattern
|
|
106
218
|
|
|
107
219
|
Wrap sub-agents as `Tool::Base` subclasses so the orchestrator LLM can call them on demand.
|
|
@@ -116,6 +228,11 @@ class ResearchTool < Phronomy::Tool::Base
|
|
|
116
228
|
end
|
|
117
229
|
end
|
|
118
230
|
|
|
231
|
+
class WriterAgent < Phronomy::Agent::Base
|
|
232
|
+
model "gpt-4o"
|
|
233
|
+
instructions "You are a professional technical writer."
|
|
234
|
+
end
|
|
235
|
+
|
|
119
236
|
class WriteTool < Phronomy::Tool::Base
|
|
120
237
|
description "Write a technical blog post given research notes and a writing brief."
|
|
121
238
|
param :instructions, type: :string, desc: "Writing brief including research notes"
|
|
@@ -137,6 +254,9 @@ puts result[:output]
|
|
|
137
254
|
|
|
138
255
|
### Guardrails — Input/output validation
|
|
139
256
|
|
|
257
|
+
Call `fail!(reason)` inside `check` to reject — it raises `Phronomy::GuardrailError`.
|
|
258
|
+
When a guardrail rejects, `invoke` raises instead of returning an output.
|
|
259
|
+
|
|
140
260
|
```ruby
|
|
141
261
|
class NoSensitiveDataGuardrail < Phronomy::Guardrail::InputGuardrail
|
|
142
262
|
def check(input)
|
|
@@ -146,8 +266,20 @@ end
|
|
|
146
266
|
|
|
147
267
|
agent = ResearchAgent.new
|
|
148
268
|
agent.add_input_guardrail(NoSensitiveDataGuardrail.new)
|
|
269
|
+
|
|
270
|
+
begin
|
|
271
|
+
agent.invoke("Charge 4111-1111-1111-1111")
|
|
272
|
+
rescue Phronomy::GuardrailError => e
|
|
273
|
+
puts e.message # => "Credit card numbers are not allowed"
|
|
274
|
+
end
|
|
149
275
|
```
|
|
150
276
|
|
|
277
|
+
> **Note:** Phronomy includes `PromptInjectionGuardrail`, a built-in pattern-based
|
|
278
|
+
> input guardrail that detects common injection patterns (see the feature table above).
|
|
279
|
+
> PII scanning and content classification are **not** provided by the framework;
|
|
280
|
+
> that logic must be implemented by the application. Reference implementations for
|
|
281
|
+
> common patterns are available in `phronomy-examples` (example 06).
|
|
282
|
+
|
|
151
283
|
### Knowledge/RAG — Context injection and vector retrieval
|
|
152
284
|
|
|
153
285
|
```ruby
|
|
@@ -161,6 +293,13 @@ policy = Phronomy::KnowledgeSource::StaticKnowledge.new(
|
|
|
161
293
|
# RAG retrieval from a vector store
|
|
162
294
|
store = Phronomy::VectorStore::InMemory.new
|
|
163
295
|
embeddings = Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small")
|
|
296
|
+
|
|
297
|
+
# Add documents before querying
|
|
298
|
+
text1 = "Refunds are processed within 5 business days."
|
|
299
|
+
text2 = "Contact support@example.com for refund requests."
|
|
300
|
+
store.add(id: "doc-1", embedding: embeddings.embed(text1), metadata: { content: text1, source: "policy.md" })
|
|
301
|
+
store.add(id: "doc-2", embedding: embeddings.embed(text2), metadata: { content: text2, source: "policy.md" })
|
|
302
|
+
|
|
164
303
|
rag = Phronomy::KnowledgeSource::RAGKnowledge.new(store: store, embeddings: embeddings, k: 5)
|
|
165
304
|
|
|
166
305
|
# Inject at invocation time
|
|
@@ -168,6 +307,15 @@ result = MyAgent.new.invoke("What is the refund policy?",
|
|
|
168
307
|
config: { knowledge_sources: [policy, rag] })
|
|
169
308
|
```
|
|
170
309
|
|
|
310
|
+
`static_knowledge_refresh!` invalidates the class-level cache of *static* knowledge sources
|
|
311
|
+
(not RAG stores). Call it when the underlying file or content has changed:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# Static knowledge sources are cached at the class level after the first fetch.
|
|
315
|
+
# Call refresh! when the underlying content changes (e.g. after reloading policy.md).
|
|
316
|
+
MyAgent.static_knowledge_refresh!
|
|
317
|
+
```
|
|
318
|
+
|
|
171
319
|
Load and split documents with built-in loaders:
|
|
172
320
|
|
|
173
321
|
```ruby
|
|
@@ -211,7 +359,7 @@ Phronomy.configure do |c|
|
|
|
211
359
|
end
|
|
212
360
|
```
|
|
213
361
|
|
|
214
|
-
Hooks are called in order — global → class → instance — and
|
|
362
|
+
Hooks are called in order — global → class → instance — and shallow-merged (`Hash#merge`; last hook wins on key conflicts).
|
|
215
363
|
|
|
216
364
|
### GeneratorVerifier — Generator-Verifier loop with custom prompt builders
|
|
217
365
|
|
|
@@ -290,19 +438,21 @@ class MyOrchestrator < Phronomy::Agent::Orchestrator
|
|
|
290
438
|
instructions "Orchestrate."
|
|
291
439
|
|
|
292
440
|
def run(query)
|
|
293
|
-
# Heterogeneous agents in parallel (cap at 4 threads; skip failures)
|
|
441
|
+
# Heterogeneous agents in parallel (cap at 4 threads; skip failures; 30 s timeout)
|
|
294
442
|
results = dispatch_parallel(
|
|
295
443
|
{agent: SearchAgent, input: "topic A"},
|
|
296
444
|
{agent: AnalysisAgent, input: query},
|
|
297
445
|
max_concurrency: 4,
|
|
298
|
-
on_error: :skip
|
|
446
|
+
on_error: :skip,
|
|
447
|
+
timeout: 30
|
|
299
448
|
)
|
|
300
449
|
|
|
301
450
|
# Fan-out — same agent, multiple inputs
|
|
302
451
|
translations = fan_out(
|
|
303
452
|
agent: TranslationAgent,
|
|
304
453
|
inputs: %w[Hello World],
|
|
305
|
-
max_concurrency: 2
|
|
454
|
+
max_concurrency: 2,
|
|
455
|
+
timeout: 20
|
|
306
456
|
)
|
|
307
457
|
|
|
308
458
|
results.compact.map { |r| r[:output] }.join("\n")
|
|
@@ -310,9 +460,11 @@ class MyOrchestrator < Phronomy::Agent::Orchestrator
|
|
|
310
460
|
end
|
|
311
461
|
```
|
|
312
462
|
|
|
313
|
-
### Workflow
|
|
463
|
+
### Workflow parallel pattern — Concurrent branches
|
|
314
464
|
|
|
315
|
-
Phronomy does not provide a
|
|
465
|
+
Phronomy does not provide a dedicated parallel-node primitive. The recommended
|
|
466
|
+
pattern for concurrent branches is to use application-level Ruby threads inside
|
|
467
|
+
a `state` action:
|
|
316
468
|
|
|
317
469
|
```ruby
|
|
318
470
|
class EnrichContext
|
|
@@ -324,13 +476,16 @@ end
|
|
|
324
476
|
app = Phronomy::Workflow.define(EnrichContext) do
|
|
325
477
|
initial :enrich
|
|
326
478
|
state :enrich, action: ->(s) do
|
|
327
|
-
results
|
|
328
|
-
threads =
|
|
329
|
-
Thread.new {
|
|
330
|
-
Thread.new {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
479
|
+
# Use Thread#value to collect results safely — avoids concurrent Hash writes
|
|
480
|
+
threads = {
|
|
481
|
+
summary: Thread.new { Summarizer.call(s) },
|
|
482
|
+
tags: Thread.new { Tagger.call(s) }
|
|
483
|
+
}
|
|
484
|
+
# For bounded waits, use Thread#join(timeout_seconds); nil means timed out — handle explicitly.
|
|
485
|
+
# Do not use Timeout.timeout or Thread#kill — both inject async exceptions that bypass cleanup.
|
|
486
|
+
# Prefer CancellationToken for cooperative cancellation of Phronomy-managed tasks.
|
|
487
|
+
threads.each_value(&:join)
|
|
488
|
+
s.merge(summary: threads[:summary].value, tags: Array(threads[:tags].value))
|
|
334
489
|
end
|
|
335
490
|
transition from: :enrich, to: :__finish__
|
|
336
491
|
end
|
|
@@ -419,22 +574,100 @@ puts result2[:output] # => "Your name is Alice."
|
|
|
419
574
|
`result[:messages]` contains the complete message history after each invocation.
|
|
420
575
|
Persist it however suits your application (in-memory hash, Redis, ActiveRecord, etc.).
|
|
421
576
|
|
|
577
|
+
> **Note on `thread_id`**: `thread_id` is a correlation identifier used internally for
|
|
578
|
+
> checkpoint/compaction context and EventLoop routing. It does **not** automatically persist or
|
|
579
|
+
> restore conversation history — you must pass `messages:` explicitly on each turn as shown above.
|
|
580
|
+
|
|
422
581
|
|
|
423
582
|
## Configuration
|
|
424
583
|
|
|
425
584
|
```ruby
|
|
426
585
|
Phronomy.configure do |c|
|
|
427
|
-
c.default_model
|
|
428
|
-
c.recursion_limit
|
|
429
|
-
c.tracer
|
|
430
|
-
c.before_completion
|
|
586
|
+
c.default_model = "gpt-4o-mini"
|
|
587
|
+
c.recursion_limit = 25
|
|
588
|
+
c.tracer = Phronomy::Tracing::NullTracer.new
|
|
589
|
+
c.before_completion = nil # optional; global hook lambda
|
|
590
|
+
c.trace_pii = false # default; set to true only when trace data contains no PII
|
|
591
|
+
c.logger = nil # optional; any object responding to #warn (e.g. Rails.logger)
|
|
592
|
+
c.event_loop_stop_grace_seconds = 5 # seconds to wait for sessions to drain on EventLoop#stop(drain: true)
|
|
593
|
+
c.runtime_backend = :thread # :thread (default); :immediate (tests, synchronous); :fiber (experimental validation only); :cooperative (deprecated alias for :immediate)
|
|
594
|
+
c.strict_runtime_guards = false # when true, raises on invoke-inside-task
|
|
595
|
+
end
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
`c.logger` receives framework diagnostic messages (e.g. unreachable-state warnings from
|
|
599
|
+
`Workflow.define`). When `nil` (default), messages are written to `$stderr` via `Kernel#warn`.
|
|
600
|
+
|
|
601
|
+
> **Note**: When `trace_pii = false`, both the _input_ and the _output_ (LLM
|
|
602
|
+
> responses and tool results) are replaced with `[REDACTED]` in trace spans.
|
|
603
|
+
> The default is `false` (PII protection enabled). Set to `true` only when
|
|
604
|
+
> trace data does not contain sensitive information.
|
|
605
|
+
|
|
606
|
+
## Sync vs Async API
|
|
607
|
+
|
|
608
|
+
Phronomy provides both synchronous and asynchronous invocation APIs.
|
|
609
|
+
Understanding when to use each prevents scheduler stalls and hidden deadlocks.
|
|
610
|
+
|
|
611
|
+
| Context | Recommended API |
|
|
612
|
+
|---------|----------------|
|
|
613
|
+
| Top-level application code, Rails controller, background job | `agent.invoke(input)` — blocks the calling thread until done |
|
|
614
|
+
| Inside a `Runtime#spawn` block, `TaskGroup`, Workflow action, Tool `execute` | `agent.invoke_async(input).await` — non-blocking within the scheduler |
|
|
615
|
+
|
|
616
|
+
### Why this matters
|
|
617
|
+
|
|
618
|
+
`invoke` is a synchronous wrapper that calls `invoke_async` and then _blocks_ the calling
|
|
619
|
+
thread until the task completes. When called from **inside** an active scheduler task, the
|
|
620
|
+
calling task blocks the scheduler thread, preventing other tasks from making progress — a
|
|
621
|
+
hidden deadlock when all scheduler threads are occupied.
|
|
622
|
+
|
|
623
|
+
### Runtime guard
|
|
624
|
+
|
|
625
|
+
Phronomy detects this pattern automatically:
|
|
626
|
+
|
|
627
|
+
```ruby
|
|
628
|
+
# Default (soft mode): logs a warning and continues
|
|
629
|
+
Phronomy.configure { |c| c.strict_runtime_guards = false }
|
|
630
|
+
|
|
631
|
+
# Strict mode: raises SchedulerReentrancyError immediately
|
|
632
|
+
Phronomy.configure { |c| c.strict_runtime_guards = true }
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
You can also query the current context directly:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
Phronomy::Runtime.in_scheduler_context? # => true if called from inside a task
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Migration: invoke → invoke_async
|
|
642
|
+
|
|
643
|
+
```ruby
|
|
644
|
+
# Before (blocks scheduler if called from inside a task)
|
|
645
|
+
result = my_agent.invoke("Hello")
|
|
646
|
+
|
|
647
|
+
# After (safe inside tasks and TaskGroups)
|
|
648
|
+
result = my_agent.invoke_async("Hello").await
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### :immediate backend (synchronous / test mode)
|
|
652
|
+
|
|
653
|
+
The `:immediate` backend runs tasks synchronously using `FakeScheduler`
|
|
654
|
+
(backed by `Task::ImmediateBackend`). Blocking I/O is isolated in `BlockingAdapterPool`.
|
|
655
|
+
To switch back to the default thread-per-task backend:
|
|
656
|
+
|
|
657
|
+
```ruby
|
|
658
|
+
Phronomy.configure { |c| c.runtime_backend = :thread }
|
|
659
|
+
# or per-example using SchedulerHelpers:
|
|
660
|
+
include Phronomy::Testing::SchedulerHelpers
|
|
661
|
+
with_fake_scheduler do |sched|
|
|
662
|
+
# all spawns run synchronously; sched.event_log records every lifecycle event
|
|
431
663
|
end
|
|
432
664
|
```
|
|
433
665
|
|
|
434
666
|
## Context Management
|
|
435
667
|
|
|
436
|
-
Phronomy includes a context window management layer
|
|
437
|
-
|
|
668
|
+
Phronomy includes a context window management layer. When model metadata is
|
|
669
|
+
available (either from the built-in registry or via an explicit `context_window:` setting),
|
|
670
|
+
agents automatically stay within the configured token limit.
|
|
438
671
|
|
|
439
672
|
### TokenBudget
|
|
440
673
|
|
|
@@ -466,12 +699,86 @@ class MyAgent < Phronomy::Agent::Base
|
|
|
466
699
|
model "gpt-4o"
|
|
467
700
|
max_output_tokens 4096 # override max_output_tokens from registry
|
|
468
701
|
context_overhead 600 # extra reservation for system prompt + tools
|
|
702
|
+
invoke_timeout 30 # raise Phronomy::TimeoutError after 30 s (wait timeout, not cancellation)
|
|
703
|
+
max_parallel_tools 4 # cap concurrent tool executions (default: 10)
|
|
469
704
|
end
|
|
470
705
|
```
|
|
471
706
|
|
|
472
707
|
`Agent::Base#invoke` builds a `TokenBudget` automatically. When the model is not in the
|
|
473
708
|
registry the budget is silently skipped.
|
|
474
709
|
|
|
710
|
+
> **Note on CJK languages**: The default `TokenEstimator` uses a character-ratio heuristic
|
|
711
|
+
> calibrated for ASCII/Latin text (4 chars/token). For Chinese, Japanese, and Korean text,
|
|
712
|
+
> actual token counts are approximately **4× higher** than the estimate because CJK
|
|
713
|
+
> characters are typically 1 token each. For accurate CJK token counting, supply a
|
|
714
|
+
> tokenizer-backed callable:
|
|
715
|
+
>
|
|
716
|
+
> ```ruby
|
|
717
|
+
> require "tiktoken_ruby"
|
|
718
|
+
> enc = Tiktoken.encoding_for_model("gpt-4o")
|
|
719
|
+
> Phronomy::Context::TokenEstimator.tokenizer = ->(text) { enc.encode(text).length }
|
|
720
|
+
> ```
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
### CancellationToken — Cooperative cancellation
|
|
724
|
+
|
|
725
|
+
Pass a `CancellationToken` to any agent via `config: { cancellation_token: token }`.
|
|
726
|
+
Cancellation is checked at multiple granular checkpoints: before the LLM call, before
|
|
727
|
+
each RAG knowledge-source fetch, after each streaming chunk, before each parallel
|
|
728
|
+
tool-call batch, and after each `before_completion` hook. `CancellationError` is
|
|
729
|
+
raised immediately and is never retried. No threads are force-killed — `ensure`
|
|
730
|
+
blocks always execute.
|
|
731
|
+
|
|
732
|
+
> **Cooperative cancellation — not preemptive**
|
|
733
|
+
>
|
|
734
|
+
> Phronomy uses _cooperative boundary cancellation_. The token is polled at the
|
|
735
|
+
> checkpoints listed above; it is **not** injected as a signal into a running
|
|
736
|
+
> operation. This means the following are **not** interrupted mid-execution:
|
|
737
|
+
>
|
|
738
|
+
> - A single `KnowledgeSource#fetch` that is already blocking (e.g. HTTP call)
|
|
739
|
+
> - A single `chat.ask` call that is not streaming
|
|
740
|
+
> - A single `tool.execute` call that is already running
|
|
741
|
+
> - Any external I/O (database query, vector search, HTTP request) inside those calls
|
|
742
|
+
>
|
|
743
|
+
> For deep in-flight safety, complement `CancellationToken` with per-source or
|
|
744
|
+
> per-tool timeouts. Prefer library-native timeouts such as `Net::HTTP#read_timeout`,
|
|
745
|
+
> database `statement_timeout`, or Redis client timeout — these signal the I/O layer
|
|
746
|
+
> to abort cleanly. Avoid `Timeout.timeout` unless you understand its async-exception
|
|
747
|
+
> risks: it injects `Timeout::Error` at an arbitrary execution point (the same
|
|
748
|
+
> mechanism as `Thread#kill`), which Phronomy avoids by default due to resource
|
|
749
|
+
> safety concerns. Ruby's GVL prevents fully preemptive cancellation without such
|
|
750
|
+
> risky interruption.
|
|
751
|
+
|
|
752
|
+
```ruby
|
|
753
|
+
token = Phronomy::CancellationToken.new
|
|
754
|
+
|
|
755
|
+
# Cancel from another thread after 5 s
|
|
756
|
+
Thread.new { sleep 5; token.cancel! }
|
|
757
|
+
|
|
758
|
+
begin
|
|
759
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
760
|
+
rescue Phronomy::CancellationError
|
|
761
|
+
puts "cancelled"
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Hard deadline via monotonic clock (recommended — immune to NTP/DST changes)
|
|
765
|
+
token = Phronomy::CancellationToken.timeout_after(30)
|
|
766
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
767
|
+
|
|
768
|
+
# Hard deadline via wall-clock (legacy — still supported)
|
|
769
|
+
token = Phronomy::CancellationToken.new(deadline: Time.now + 30)
|
|
770
|
+
result = MyAgent.new.invoke("...", config: { cancellation_token: token })
|
|
771
|
+
|
|
772
|
+
# Propagate to all parallel workers via dispatch_parallel / fan_out
|
|
773
|
+
token = Phronomy::CancellationToken.new
|
|
774
|
+
Thread.new { sleep 10; token.cancel! }
|
|
775
|
+
|
|
776
|
+
orchestrator.dispatch_parallel(
|
|
777
|
+
{agent: SearchAgent, input: "topic A"},
|
|
778
|
+
{agent: AnalysisAgent, input: "topic B"},
|
|
779
|
+
cancellation_token: token
|
|
780
|
+
)
|
|
781
|
+
```
|
|
475
782
|
|
|
476
783
|
## Examples
|
|
477
784
|
|
|
@@ -542,6 +849,37 @@ bin/console
|
|
|
542
849
|
|
|
543
850
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Raizo-TCS/phronomy.
|
|
544
851
|
|
|
852
|
+
## Security & Privacy
|
|
853
|
+
|
|
854
|
+
**API credentials** — Phronomy does not store or transmit your LLM API keys. All
|
|
855
|
+
credentials are handled by RubyLLM and passed directly to the provider.
|
|
856
|
+
|
|
857
|
+
**Tracing and PII** — When tracing is enabled (`Phronomy::Tracing::OpenTelemetryTracer`
|
|
858
|
+
or a custom tracer), agent inputs and LLM outputs are replaced with `[REDACTED]` in
|
|
859
|
+
span attributes by default (`trace_pii: false`). To include full content in traces
|
|
860
|
+
(e.g., for debugging in a non-production environment), set `trace_pii: true` in your
|
|
861
|
+
Phronomy configuration. Evaluate whether your tracing backend (OTLP collector, Jaeger,
|
|
862
|
+
Honeycomb, etc.) meets your data-retention and privacy requirements.
|
|
863
|
+
|
|
864
|
+
**Prompt injection** — Phronomy provides `PromptInjectionGuardrail`, a built-in
|
|
865
|
+
pattern-based input guardrail that detects common injection patterns (ignore/override
|
|
866
|
+
instructions, role-switching phrases, etc.). It is a useful starting point, not a
|
|
867
|
+
comprehensive defence; applications processing untrusted input should layer additional
|
|
868
|
+
custom guardrails as needed (see the Guardrails section above).
|
|
869
|
+
|
|
870
|
+
**Tool and MCP security** — Tools can perform real-world side effects (database
|
|
871
|
+
writes, API calls, file deletion). Treat tool execution as a privileged operation:
|
|
872
|
+
use the interrupt/approval mechanism for high-risk tools (e.g., payment processing,
|
|
873
|
+
file deletion) rather than allowing fully autonomous execution. MCP servers are
|
|
874
|
+
external trust boundaries: connect only to servers you control. A compromised MCP
|
|
875
|
+
server can inject instructions that manipulate agent behavior (tool-level prompt
|
|
876
|
+
injection). Avoid passing secrets as direct tool parameters — if `trace_pii: true`
|
|
877
|
+
is set, tool arguments are captured in trace spans.
|
|
878
|
+
|
|
879
|
+
**Vulnerability reports** — Please report security vulnerabilities privately via
|
|
880
|
+
GitHub's [Security Advisories](https://github.com/Raizo-TCS/phronomy/security/advisories)
|
|
881
|
+
rather than opening a public issue.
|
|
882
|
+
|
|
545
883
|
## License
|
|
546
884
|
|
|
547
885
|
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
|