phronomy 0.22.0 → 0.23.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 +3 -4
- data/CHANGELOG.md +200 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +152 -0
- data/docs/architecture/persistence.md +175 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +82 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/README.md +172 -0
- data/docs/features.md +31 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +145 -72
- data/lib/phronomy/agent/base.rb +388 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +336 -102
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime.rb +77 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Benchmark:
|
|
3
|
+
# Benchmark: typed ContextPolicyInput and split Context assembly.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
6
|
# ruby benchmark/bench_context_assembler.rb
|
|
7
7
|
#
|
|
8
|
-
# Measures:
|
|
9
|
-
# 1. ContextPolicies::Default selection cost for growing canonical candidate sets.
|
|
10
|
-
# 2. ContextAssembler#build_initial end-to-end Manifest construction.
|
|
11
|
-
#
|
|
12
8
|
# No provider call is performed.
|
|
13
9
|
|
|
14
10
|
require "benchmark"
|
|
@@ -17,66 +13,40 @@ require_relative "../lib/phronomy"
|
|
|
17
13
|
module BenchContextAssembler
|
|
18
14
|
module_function
|
|
19
15
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
[:knowledge, :user]
|
|
23
|
-
elsif index.even?
|
|
24
|
-
[:assistant_message, :assistant]
|
|
25
|
-
else
|
|
26
|
-
[:external_message, :user]
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
Phronomy::Agent::ContextCandidate.new(
|
|
30
|
-
candidate_id: "candidate-#{index}",
|
|
31
|
-
source_kind: :journal,
|
|
32
|
-
category: category,
|
|
33
|
-
role: role,
|
|
34
|
-
content_ref: "content-#{index}",
|
|
35
|
-
record_id: "record-#{index}",
|
|
36
|
-
agent_id: "bench-agent",
|
|
37
|
-
execution_id: "execution-#{index / 4}",
|
|
38
|
-
llm_call_id: nil,
|
|
39
|
-
tool_call_id: nil,
|
|
40
|
-
sequence: index,
|
|
41
|
-
requirement: :optional,
|
|
42
|
-
priority: 0,
|
|
43
|
-
metadata: {
|
|
44
|
-
"estimated_tokens" => 8,
|
|
45
|
-
"source_sequence" => index
|
|
46
|
-
}
|
|
47
|
-
)
|
|
16
|
+
def provenance
|
|
17
|
+
Phronomy::Agent::ContextPolicyInput::Provenance.new(origin: :journal)
|
|
48
18
|
end
|
|
49
19
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Phronomy::Agent::
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
20
|
+
def policy_input(item_count)
|
|
21
|
+
knowledge = []
|
|
22
|
+
conversation = []
|
|
23
|
+
item_count.times do |index|
|
|
24
|
+
if (index % 10).zero?
|
|
25
|
+
knowledge << Phronomy::Agent::ContextPolicyInput::KnowledgeItem.new(
|
|
26
|
+
id: "knowledge-#{index}", kind: :knowledge, role: :user,
|
|
27
|
+
content: "knowledge #{index}", content_format: :text,
|
|
28
|
+
estimated_tokens: 8, required: false, provenance: provenance, metadata: {}
|
|
29
|
+
)
|
|
30
|
+
else
|
|
31
|
+
conversation << [Phronomy::Agent::ContextPolicyInput::ConversationItem.new(
|
|
32
|
+
id: "message-#{index}", kind: :external_message, role: :user,
|
|
33
|
+
content: "message #{index}", content_format: :text,
|
|
34
|
+
sequence: index, estimated_tokens: 8, required: false,
|
|
35
|
+
provenance: provenance, tool_call_id: nil, tool_call_ids: [],
|
|
36
|
+
delivery: :chat_message, metadata: {}
|
|
37
|
+
)]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
execution_id: "bench-execution",
|
|
68
|
-
call_sequence: 2,
|
|
69
|
-
call_mode: :complete,
|
|
70
|
-
candidates: candidates,
|
|
41
|
+
Phronomy::Agent::ContextPolicyInput.new(
|
|
42
|
+
agent_id: "bench-agent", execution_id: "bench-execution",
|
|
43
|
+
call_sequence: 2, call_mode: :complete,
|
|
44
|
+
instruction: [], knowledge: knowledge, tools: [], conversation: conversation,
|
|
71
45
|
token_budget: Phronomy::LlmContextWindow::TokenBudget.new(
|
|
72
|
-
context_window: [
|
|
46
|
+
context_window: [item_count * 16, 4_096].max,
|
|
73
47
|
max_output_tokens: 512
|
|
74
48
|
),
|
|
75
|
-
model_config: {},
|
|
76
|
-
previous_manifest: nil,
|
|
77
|
-
required_coverage: [],
|
|
78
|
-
parts: parts,
|
|
79
|
-
metadata: {"mandatory_token_estimate" => 32}
|
|
49
|
+
model_config: {}, previous_manifest: nil, metadata: {}
|
|
80
50
|
)
|
|
81
51
|
end
|
|
82
52
|
|
|
@@ -124,31 +94,32 @@ module BenchContextAssembler
|
|
|
124
94
|
end
|
|
125
95
|
end
|
|
126
96
|
|
|
127
|
-
puts "
|
|
97
|
+
puts "Typed Context Policy benchmark"
|
|
128
98
|
puts "Ruby #{RUBY_VERSION} on #{RUBY_PLATFORM}"
|
|
129
99
|
puts "=" * 72
|
|
130
100
|
|
|
131
|
-
policy = Phronomy::Agent::ContextPolicies::Default.
|
|
132
|
-
|
|
133
|
-
[count, BenchContextAssembler.
|
|
101
|
+
policy = Phronomy::Agent::ContextPolicies::Default.instance
|
|
102
|
+
policy_inputs = [10, 100, 1_000].to_h do |count|
|
|
103
|
+
[count, BenchContextAssembler.policy_input(count)]
|
|
134
104
|
end
|
|
135
105
|
|
|
136
106
|
Benchmark.bm(46) do |x|
|
|
137
|
-
|
|
107
|
+
policy_inputs.each do |count, policy_input|
|
|
138
108
|
iterations = (count >= 1_000) ? 200 : 1_000
|
|
139
|
-
x.report("DefaultContextPolicy #{count}
|
|
140
|
-
iterations.times { policy.call(
|
|
109
|
+
x.report("DefaultContextPolicy #{count} items x#{iterations}") do
|
|
110
|
+
iterations.times { policy.call(policy_input) }
|
|
141
111
|
end
|
|
142
112
|
end
|
|
143
113
|
|
|
144
114
|
assembler, root, execution = BenchContextAssembler.assembler_fixture
|
|
145
|
-
x.report("ContextAssembler
|
|
115
|
+
x.report("ContextAssembler prepare/finalize x500") do
|
|
146
116
|
500.times do
|
|
147
|
-
assembler.
|
|
117
|
+
prepared = assembler.prepare_initial(
|
|
148
118
|
input: "benchmark input",
|
|
149
119
|
agent_root: root,
|
|
150
120
|
execution: execution
|
|
151
121
|
)
|
|
122
|
+
assembler.finalize(prepared)
|
|
152
123
|
end
|
|
153
124
|
end
|
|
154
125
|
end
|
|
@@ -97,7 +97,7 @@ end
|
|
|
97
97
|
stub_agent_class = Class.new(Phronomy::Agent::Base) do
|
|
98
98
|
agent_definition id: "bench-stub", version: 1
|
|
99
99
|
|
|
100
|
-
define_method(:invoke) do |_input,
|
|
100
|
+
define_method(:invoke) do |_input, config: {}|
|
|
101
101
|
{output: "stub", messages: []}
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -112,7 +112,7 @@ stub_agent_class = Class.new(Phronomy::Agent::Base) do
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
orchestrator_class = Class.new(Phronomy::MultiAgent::Orchestrator)
|
|
115
|
+
orchestrator_class = Class.new(Phronomy::MultiAgent::Orchestrator) { agent_definition id: "orchestrator", version: 1 }
|
|
116
116
|
orchestrator = orchestrator_class.new
|
|
117
117
|
|
|
118
118
|
PARALLEL_ITERATIONS = 200
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
> **CURRENT explanatory architecture**
|
|
2
|
+
>
|
|
3
|
+
> This document describes the reconciled current Phronomy system. Normative
|
|
4
|
+
> architecture decisions remain in the [ADR index](../decisions/README.md);
|
|
5
|
+
> source/runtime behavior remains implementation reality and does not silently
|
|
6
|
+
> amend an ADR.
|
|
7
|
+
|
|
8
|
+
# Agent Context, Identity, and Ownership
|
|
9
|
+
|
|
10
|
+
## 1. System boundary
|
|
11
|
+
|
|
12
|
+
A stateful Agent separates canonical durable facts, one live Runtime owner, and
|
|
13
|
+
the logical input finalized for each Provider call.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Application
|
|
17
|
+
|
|
|
18
|
+
live Agent instance -- agent_id --> one Runtime-local mutable owner
|
|
19
|
+
|
|
|
20
|
+
+-- AgentRoot / Journal view
|
|
21
|
+
|
|
|
22
|
+
+-- AgentExecution -- execution_id
|
|
23
|
+
|
|
|
24
|
+
+-- FSMSession incarnation -- fsm_session_id
|
|
25
|
+
|
|
|
26
|
+
+-- Provider Call -- llm_call_id
|
|
27
|
+
|
|
|
28
|
+
+-- ToolInvocation -- tool_invocation_id / tool_call_id
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`agent_id` identifies the logical Agent. `execution_id` identifies one logical
|
|
32
|
+
Agent execution. `fsm_session_id` identifies one Runtime execution incarnation.
|
|
33
|
+
Provider and Tool semantic IDs identify their own operations. These identities
|
|
34
|
+
are intentionally distinct.
|
|
35
|
+
|
|
36
|
+
Phronomy does not restore a generic Agent `thread_id`, `session_id`,
|
|
37
|
+
`correlation_id`, or `agent_invocation_id` identity. Application observability
|
|
38
|
+
may use `InvocationContext#task_id` / `#parent_task_id`, but those values are not
|
|
39
|
+
Agent, Workflow, Tool, or Runtime domain identities.
|
|
40
|
+
|
|
41
|
+
Relevant decisions:
|
|
42
|
+
[ADR-021](../decisions/021-generic-agent-invocation-identity-removal.md),
|
|
43
|
+
[ADR-022](../decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md),
|
|
44
|
+
and [ADR-023](../decisions/023-fsm-session-incarnation-identity-and-routing.md).
|
|
45
|
+
|
|
46
|
+
## 2. Canonical history and one-call input
|
|
47
|
+
|
|
48
|
+
The Agent Journal is append-only canonical execution history. It is not the
|
|
49
|
+
Provider message buffer.
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
Journal / registered Knowledge / current-call material
|
|
53
|
+
|
|
|
54
|
+
v
|
|
55
|
+
ContextPolicyInput
|
|
56
|
+
instruction / knowledge / tools / conversation
|
|
57
|
+
|
|
|
58
|
+
v
|
|
59
|
+
ContextPolicy
|
|
60
|
+
|
|
|
61
|
+
v
|
|
62
|
+
ContextPlan
|
|
63
|
+
|
|
|
64
|
+
v
|
|
65
|
+
ContextAssembler
|
|
66
|
+
|
|
|
67
|
+
v
|
|
68
|
+
LLMInputManifest
|
|
69
|
+
|
|
|
70
|
+
v
|
|
71
|
+
RubyLLMMaterializer
|
|
72
|
+
|
|
|
73
|
+
v
|
|
74
|
+
Provider
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The finalized `LLMInputManifest` is the logical authority for one Provider Call.
|
|
78
|
+
The Journal remains the canonical history authority. Context selection never
|
|
79
|
+
rewrites old Journal facts.
|
|
80
|
+
|
|
81
|
+
See [Context Management](context-management.md) and
|
|
82
|
+
[ADR-012](../decisions/012-canonical-execution-log-and-context-policy.md).
|
|
83
|
+
|
|
84
|
+
## 3. Live mutable ownership
|
|
85
|
+
|
|
86
|
+
Within one Runtime/process, one mutable live Agent instance owns one `agent_id`.
|
|
87
|
+
Loading or creating a second mutable live instance for the same `agent_id` is
|
|
88
|
+
rejected.
|
|
89
|
+
|
|
90
|
+
During active execution, EventLoop is the single writer of Phronomy-managed live
|
|
91
|
+
Agent execution state. Blocking Persistence work and other synchronous work that
|
|
92
|
+
must not run on EventLoop are submitted to OffloadPool. Worker results return as
|
|
93
|
+
immutable operation-specific results and are applied only after EventLoop checks
|
|
94
|
+
that the current execution/session/semantic-operation authority still matches.
|
|
95
|
+
|
|
96
|
+
Persistence conflict detection protects durable state from stale writes. It is
|
|
97
|
+
not a substitute for Runtime ownership.
|
|
98
|
+
|
|
99
|
+
See
|
|
100
|
+
[ADR-024](../decisions/024-event-loop-single-writer-agent-runtime.md) and
|
|
101
|
+
[ADR-025](../decisions/025-process-local-agent-ownership-and-runtime-admission.md).
|
|
102
|
+
|
|
103
|
+
## 4. Same-process and cross-process responsibility
|
|
104
|
+
|
|
105
|
+
Same-process competing top-level Agent execution is rejected by Runtime
|
|
106
|
+
admission. Cross-process competing-execution exclusion is **conditional**, not an
|
|
107
|
+
unconditional Phronomy guarantee.
|
|
108
|
+
|
|
109
|
+
A multi-process deployment must arrange stable routing/partitioning by logical
|
|
110
|
+
identity or use an external coordination mechanism that establishes exclusive
|
|
111
|
+
authority. Optimistic Persistence CAS/revision checks detect stale durable
|
|
112
|
+
transitions; they do not themselves provide distributed exclusion.
|
|
113
|
+
|
|
114
|
+
Phronomy does not currently add a distributed lease/fencing coordinator, live
|
|
115
|
+
Agent migration protocol, or automatic takeover subsystem merely because
|
|
116
|
+
Persistence is durable.
|
|
117
|
+
|
|
118
|
+
See
|
|
119
|
+
[ADR-018](../decisions/018-durability-guarantees-and-failure-model.md).
|
|
120
|
+
|
|
121
|
+
## 5. Suspension, recovery, and process loss
|
|
122
|
+
|
|
123
|
+
A live approval suspension retains the logical Agent execution slot. A later
|
|
124
|
+
approval resumes the same logical `execution_id` with a new Runtime
|
|
125
|
+
FSMSession incarnation as needed.
|
|
126
|
+
|
|
127
|
+
After Runtime/process loss, process-local objects are gone. `Agent.load` uses
|
|
128
|
+
confirmed durable state to classify unfinished work as:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
resumable
|
|
132
|
+
reconcilable
|
|
133
|
+
resolution_required
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Recovery does not restore the old Ruby object graph or old Task callbacks. It
|
|
137
|
+
reconstructs current logical state from durable evidence and continues only
|
|
138
|
+
when authority and outcome certainty permit it.
|
|
139
|
+
|
|
140
|
+
A finalized historical Manifest is reused for the Provider input it represents;
|
|
141
|
+
Recovery does not rerun the historical Context Policy merely to reconstruct that
|
|
142
|
+
Manifest.
|
|
143
|
+
|
|
144
|
+
See [Persistence](persistence.md) for the durability/recovery boundary.
|
|
145
|
+
|
|
146
|
+
## 6. Multi-Agent responsibility
|
|
147
|
+
|
|
148
|
+
A semantic Handoff changes which live Agent is responsible for continuing the
|
|
149
|
+
interaction. It does not merge Source and Target canonical state.
|
|
150
|
+
|
|
151
|
+
Transferred Handoff Context is immutable request-scoped material. The Target
|
|
152
|
+
Context Policy may select it for a Target LLM Call, but Handoff does not
|
|
153
|
+
automatically append it to Target Journal or persistent Knowledge.
|
|
154
|
+
|
|
155
|
+
Active Target responsibility persists across later Runner turns only within the
|
|
156
|
+
same Runtime/main-Agent coordination lifetime. It is not durably rehydrated
|
|
157
|
+
after Runtime/process reset.
|
|
158
|
+
|
|
159
|
+
See [Multi-Agent Handoff](multi-agent-handoff.md).
|
|
160
|
+
|
|
161
|
+
## 7. Removed models
|
|
162
|
+
|
|
163
|
+
The following are not current Agent Context authorities:
|
|
164
|
+
|
|
165
|
+
- mutable RubyLLM message history as canonical Agent state;
|
|
166
|
+
- `Memory::ConversationManager`;
|
|
167
|
+
- `LlmContextWindow::Assembler`;
|
|
168
|
+
- `ContextVersionCache`;
|
|
169
|
+
- a Static/Entity/RAG Knowledge source hierarchy;
|
|
170
|
+
- a generic Agent conversation/thread/session identity;
|
|
171
|
+
- cross-Agent Journal sharing as Handoff semantics.
|
|
172
|
+
|
|
173
|
+
See [Removed Agent Context Architecture](removed/agent-context.md) for the
|
|
174
|
+
negative guidance retained from the retired designs.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
> **CURRENT explanatory architecture**
|
|
2
|
+
>
|
|
3
|
+
> This document describes the reconciled current Phronomy system. Normative
|
|
4
|
+
> architecture decisions remain in the [ADR index](../decisions/README.md);
|
|
5
|
+
> source/runtime behavior remains implementation reality and does not silently
|
|
6
|
+
> amend an ADR.
|
|
7
|
+
|
|
8
|
+
# before_llm_input
|
|
9
|
+
|
|
10
|
+
## 1. Purpose
|
|
11
|
+
|
|
12
|
+
`before_llm_input` is the supported request-scoped customization boundary before
|
|
13
|
+
a logical LLM input is finalized.
|
|
14
|
+
|
|
15
|
+
It may adjust model configuration and add logical Context candidates without
|
|
16
|
+
mutating Agent Journal state or a RubyLLM message buffer.
|
|
17
|
+
|
|
18
|
+
## 2. Registration tiers
|
|
19
|
+
|
|
20
|
+
Hooks may be configured globally, on an Agent class, or on one Agent instance.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
global -> class -> instance
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Later model-configuration patches take precedence for the same key. Segment
|
|
27
|
+
candidates are accumulated in hook order.
|
|
28
|
+
|
|
29
|
+
## 3. Input and result
|
|
30
|
+
|
|
31
|
+
Hooks receive immutable `Phronomy::Agent::LLMInputBuildContext` metadata. They do
|
|
32
|
+
not receive a mutable Provider chat/message array.
|
|
33
|
+
|
|
34
|
+
A hook returns `Phronomy::Agent::LLMInputPatch` or `nil`.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Phronomy::Agent::LLMInputPatch.new(
|
|
38
|
+
model_config_patch: {temperature: 0.2},
|
|
39
|
+
segment_candidates: [
|
|
40
|
+
{
|
|
41
|
+
category: :knowledge,
|
|
42
|
+
role: :user,
|
|
43
|
+
content: "request-scoped retrieved context"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 4. Context Policy boundary
|
|
50
|
+
|
|
51
|
+
`segment_candidates` are candidate material, not preselected Manifest segments.
|
|
52
|
+
|
|
53
|
+
They enter the same typed `ContextPolicyInput` path and may be selected, omitted,
|
|
54
|
+
or otherwise handled by Application Policy subject to Framework invariants.
|
|
55
|
+
|
|
56
|
+
Hook candidates are current-call material and are not automatically written to
|
|
57
|
+
Journal or persistent Knowledge.
|
|
58
|
+
|
|
59
|
+
## 5. Constraints
|
|
60
|
+
|
|
61
|
+
A hook must not:
|
|
62
|
+
|
|
63
|
+
- mutate Agent Journal state;
|
|
64
|
+
- mutate RubyLLM Chat/messages;
|
|
65
|
+
- assume an optional candidate will be selected;
|
|
66
|
+
- forge Framework-reserved Context metadata;
|
|
67
|
+
- bypass ContextPlan validation/canonicalization; or
|
|
68
|
+
- bypass final token-budget validation.
|
|
69
|
+
|
|
70
|
+
## 6. Trust/security
|
|
71
|
+
|
|
72
|
+
Phronomy does not reinterpret `input_filter` as a universal Filter over hook
|
|
73
|
+
candidates.
|
|
74
|
+
|
|
75
|
+
Application ContextPolicy may inspect hook candidate provenance/metadata and
|
|
76
|
+
apply domain-specific trust/security policy.
|
|
77
|
+
|
|
78
|
+
See [Security Boundaries](security-boundaries.md).
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
> **CURRENT explanatory architecture**
|
|
2
|
+
>
|
|
3
|
+
> This document describes the reconciled current Phronomy system. Normative
|
|
4
|
+
> architecture decisions remain in the [ADR index](../decisions/README.md);
|
|
5
|
+
> source/runtime behavior remains implementation reality and does not silently
|
|
6
|
+
> amend an ADR.
|
|
7
|
+
|
|
8
|
+
# Context Management
|
|
9
|
+
|
|
10
|
+
## 1. Authority model
|
|
11
|
+
|
|
12
|
+
Phronomy separates canonical execution facts from the logical input of one LLM
|
|
13
|
+
Call.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Observed execution facts / registered Knowledge / current-call material
|
|
17
|
+
|
|
|
18
|
+
v
|
|
19
|
+
JournalProjection + internal normalization
|
|
20
|
+
|
|
|
21
|
+
v
|
|
22
|
+
ContextPolicyInput
|
|
23
|
+
instruction / knowledge / tools / conversation
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
ContextPolicy#call
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
ContextPlan
|
|
30
|
+
instruction / knowledge / tools / conversation
|
|
31
|
+
|
|
|
32
|
+
v
|
|
33
|
+
ContextAssembler validation / canonicalization / final budget check
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
LLMInputManifest
|
|
37
|
+
|
|
|
38
|
+
v
|
|
39
|
+
RubyLLMMaterializer
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Journal** is canonical append-only logical execution history.
|
|
43
|
+
- **ContextPolicyInput** is an immutable typed snapshot of material eligible for
|
|
44
|
+
policy consideration.
|
|
45
|
+
- **ContextPlan** expresses the Policy's semantic selection/order/generation.
|
|
46
|
+
- **LLMInputManifest** is canonical logical input for one Provider Call.
|
|
47
|
+
- **RubyLLMMaterializer** realizes that Manifest against runtime Provider/Tool
|
|
48
|
+
wiring; it is not another Context authority.
|
|
49
|
+
|
|
50
|
+
The normative Journal/Manifest split is
|
|
51
|
+
[ADR-012](../decisions/012-canonical-execution-log-and-context-policy.md).
|
|
52
|
+
|
|
53
|
+
## 2. Public Context Policy SPI
|
|
54
|
+
|
|
55
|
+
Application code supplies an ordinary reusable Ruby strategy object:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class SearchContextPolicy < Phronomy::Agent::ContextPolicy
|
|
59
|
+
def initialize(search:)
|
|
60
|
+
@search = search
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call(input)
|
|
64
|
+
plan(
|
|
65
|
+
instruction: input.instruction,
|
|
66
|
+
knowledge: @search.call(input.knowledge),
|
|
67
|
+
tools: input.tools,
|
|
68
|
+
conversation: input.conversation
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
SEARCH_POLICY = SearchContextPolicy.new(search: SEARCH_SERVICE)
|
|
74
|
+
|
|
75
|
+
class ResearchAgent < Phronomy::Agent::Base
|
|
76
|
+
context_policy SEARCH_POLICY
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`context_policy` binds a **Policy instance** on the Agent class. Policy
|
|
81
|
+
construction/dependency injection are Application responsibilities. If the same
|
|
82
|
+
instance is shared by multiple Agents/classes, its concurrency safety is also an
|
|
83
|
+
Application responsibility.
|
|
84
|
+
|
|
85
|
+
There are no Policy overrides on Agent create/load/invoke/stream and no durable
|
|
86
|
+
Policy descriptor/registry. A currently loaded Application supplies the current
|
|
87
|
+
Policy code.
|
|
88
|
+
|
|
89
|
+
## 3. ContextPolicyInput
|
|
90
|
+
|
|
91
|
+
`ContextPolicyInput` has four top-level semantic collections:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
instruction
|
|
95
|
+
knowledge
|
|
96
|
+
tools
|
|
97
|
+
conversation
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Instruction, Knowledge, Tool, and Conversation items are typed immutable values.
|
|
101
|
+
|
|
102
|
+
Conversation is an ordered outer Array of immutable non-empty inner Arrays. Each
|
|
103
|
+
inner Array is an indivisible selection group. Ordinary messages are singleton
|
|
104
|
+
groups. An assistant Tool Call and the matching Tool-role message(s) are one
|
|
105
|
+
atomic group and cannot be split by Policy selection.
|
|
106
|
+
|
|
107
|
+
Policy input also carries execution/call/model/budget information needed for
|
|
108
|
+
selection without exposing mutable Runtime state.
|
|
109
|
+
|
|
110
|
+
## 4. ContextPlan and Framework validation
|
|
111
|
+
|
|
112
|
+
The Plan uses the same four categories. Presence means selected; omission means
|
|
113
|
+
omitted; order within a category is semantically meaningful.
|
|
114
|
+
|
|
115
|
+
The Framework validates that:
|
|
116
|
+
|
|
117
|
+
- selected input items came from the immutable request;
|
|
118
|
+
- Policy-generated items use permitted generated-item rules;
|
|
119
|
+
- required material is retained;
|
|
120
|
+
- input conversation groups are not split, merged, or internally reordered;
|
|
121
|
+
- generated conversation Tool protocol is structurally valid;
|
|
122
|
+
- selected Tools resolve to the effective runtime Tool wiring;
|
|
123
|
+
- Framework-owned metadata cannot be forged by Application content; and
|
|
124
|
+
- the realized canonical input fits the final token budget.
|
|
125
|
+
|
|
126
|
+
Provider/Manifest structural placement remains `ContextAssembler`
|
|
127
|
+
responsibility.
|
|
128
|
+
|
|
129
|
+
## 5. Policy-generated / compacted material
|
|
130
|
+
|
|
131
|
+
An Application Policy may derive current-call material using the protected
|
|
132
|
+
instruction/knowledge/conversation item helpers. This is also the supported
|
|
133
|
+
shape for Application-defined semantic compaction:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
source optional Context
|
|
137
|
+
|
|
|
138
|
+
Policy omits source item(s) for this call
|
|
139
|
+
+
|
|
140
|
+
Policy creates derived current-call item
|
|
141
|
+
|
|
|
142
|
+
ContextPlan
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The original Journal facts are not deleted or rewritten. Generated content is
|
|
146
|
+
current-call material unless the Application explicitly persists equivalent
|
|
147
|
+
Knowledge through a separate supported mutation.
|
|
148
|
+
|
|
149
|
+
Phronomy does not provide a parallel mutable "Memory Compression" subsystem, a
|
|
150
|
+
fifth `derived` Plan collection, automatic summarization, or an extra built-in
|
|
151
|
+
LLM call for the Default Policy.
|
|
152
|
+
|
|
153
|
+
Required material cannot be silently compacted away. If required input cannot
|
|
154
|
+
be accepted/represented within the applicable invariants and budget, Context
|
|
155
|
+
preparation fails.
|
|
156
|
+
|
|
157
|
+
## 6. Default Policy
|
|
158
|
+
|
|
159
|
+
The built-in Default Policy is deterministic and model-free:
|
|
160
|
+
|
|
161
|
+
- retain instruction material in stable order;
|
|
162
|
+
- retain effective Tool definitions/order;
|
|
163
|
+
- retain required/current conversation;
|
|
164
|
+
- select an optional recent contiguous conversation window at group granularity;
|
|
165
|
+
- select Knowledge in stable order, skipping an oversized optional item and
|
|
166
|
+
continuing;
|
|
167
|
+
- use an approximate 60% conversation / 40% Knowledge split for variable
|
|
168
|
+
remainder and allow unused share to spill to the other category;
|
|
169
|
+
- perform no vector retrieval, embedding/reranking, or automatic compaction.
|
|
170
|
+
|
|
171
|
+
If required/fixed material does not fit, Context preparation fails rather than
|
|
172
|
+
silently dropping it.
|
|
173
|
+
|
|
174
|
+
## 7. Execution and Persistence boundary
|
|
175
|
+
|
|
176
|
+
`ContextPolicy#call` is synchronous and runs on the Agent preparation Offload
|
|
177
|
+
worker, not on EventLoop. No Phronomy Persistence transaction spans the Policy
|
|
178
|
+
call.
|
|
179
|
+
|
|
180
|
+
Preparation captures an immutable authoritative base, executes Application
|
|
181
|
+
Policy outside the durable transaction, then revalidates Agent/Execution
|
|
182
|
+
revision/watermark before the short commit/finalization path. A stale Policy
|
|
183
|
+
result fails closed rather than overwriting newer durable state.
|
|
184
|
+
|
|
185
|
+
Policy failure is not automatically retried and Phronomy does not silently fall
|
|
186
|
+
back to another Policy.
|
|
187
|
+
|
|
188
|
+
ContextPolicy is **not** one of the Framework's standard automatic tracing span
|
|
189
|
+
types. Policy-internal retrieval/transformation tracing is Application-owned.
|
|
190
|
+
See [Tracing](tracing.md).
|
|
191
|
+
|
|
192
|
+
## 8. Recovery
|
|
193
|
+
|
|
194
|
+
A finalized `LLMInputManifest` is the Recovery authority for the Provider input
|
|
195
|
+
it represents. Recovery hydrates that Manifest rather than rerunning the
|
|
196
|
+
historical Policy.
|
|
197
|
+
|
|
198
|
+
Future calls use the Policy supplied by the currently loaded Application code.
|
|
199
|
+
Agent/Workflow durability therefore does not depend on Policy object durability.
|
|
200
|
+
|
|
201
|
+
## 9. Security/trust policy
|
|
202
|
+
|
|
203
|
+
The Framework validates structure/integrity; it does not impose one universal
|
|
204
|
+
semantic trust policy on arbitrary Context.
|
|
205
|
+
|
|
206
|
+
Application-defined `ContextPolicy` is the semantic authority for source-aware
|
|
207
|
+
selection, rejection/failure, redaction/sanitization, retrieval, reranking, or
|
|
208
|
+
derived content needed for a particular LLM Call. Phronomy does not add a fourth
|
|
209
|
+
universal `context_filter` call site.
|
|
210
|
+
|
|
211
|
+
See [Security Boundaries](security-boundaries.md).
|
|
212
|
+
|
|
213
|
+
## 10. Removed intermediate contracts
|
|
214
|
+
|
|
215
|
+
The following are not current public Context Policy contracts:
|
|
216
|
+
|
|
217
|
+
- `ContextRequest`;
|
|
218
|
+
- `ContextPolicyDescriptor` / `ContextPolicyRegistry`;
|
|
219
|
+
- `DerivedContentSpec`;
|
|
220
|
+
- `ContextPlan#selected_unit_ids`;
|
|
221
|
+
- `ContextPlan#derived_contents`;
|
|
222
|
+
- `ContextPlan#ordering_hints`;
|
|
223
|
+
- `ContextPlan#policy_descriptor`;
|
|
224
|
+
- public `request.parts`;
|
|
225
|
+
- `Selection::Unit` / `Selection::Validator`;
|
|
226
|
+
- `DependencyAwareUnitBuilder`;
|
|
227
|
+
- `RequiredContextResolver`;
|
|
228
|
+
- `RecentFirstSelector`;
|
|
229
|
+
- `TokenBudgetPacker`.
|
|
230
|
+
|
|
231
|
+
Internal `Selection::Candidate` / `Selection::Constraint` normalization used
|
|
232
|
+
while building the typed request does not create an Application DSL.
|