phronomy 0.18.0 → 0.20.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/CHANGELOG.md +25 -0
- data/CONTRIBUTING.md +30 -0
- data/README.md +2 -0
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
- data/docs/features.md +27 -1
- data/docs/getting-started.md +37 -1
- data/docs/migrations/0.19.md +154 -0
- data/docs/persistence-backends.md +504 -0
- data/docs/runtime-and-concurrency.md +93 -2
- data/lib/phronomy/agent/agent_execution.rb +29 -0
- data/lib/phronomy/agent/base.rb +81 -36
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +420 -249
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/llm_call_record.rb +20 -0
- data/lib/phronomy/configuration.rb +2 -1
- data/lib/phronomy/engine/event_loop.rb +86 -8
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime.rb +7 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +109 -6
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
- data/lib/phronomy/testing/persistence_contract.rb +41 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +361 -95
- data/lib/phronomy.rb +9 -0
- metadata +12 -5
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/scripts/check_private_enforcement.rb +0 -93
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -252,24 +252,22 @@ module Phronomy
|
|
|
252
252
|
new(agent_id: agent_id, persistence: persistence, load_existing: true)
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
config: config
|
|
272
|
-
)
|
|
255
|
+
# Resolves the live Agent instance that currently owns execution_id in
|
|
256
|
+
# this process. This is a Runtime-local lookup, not durable rehydration.
|
|
257
|
+
def live_for_execution(execution_id)
|
|
258
|
+
activation = Phronomy::Runtime.instance.__agent_activations.fetch(execution_id)
|
|
259
|
+
unless activation
|
|
260
|
+
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
261
|
+
"no live activation for #{execution_id}; durable rehydration is required"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
agent = activation.agent
|
|
265
|
+
unless agent.is_a?(self)
|
|
266
|
+
raise ArgumentError,
|
|
267
|
+
"live activation #{execution_id} belongs to #{agent.class}, not #{self}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
agent
|
|
273
271
|
end
|
|
274
272
|
end
|
|
275
273
|
|
|
@@ -283,21 +281,33 @@ module Phronomy
|
|
|
283
281
|
metadata: {},
|
|
284
282
|
load_existing: false
|
|
285
283
|
)
|
|
286
|
-
@persistence = persistence ||
|
|
284
|
+
@persistence = persistence ||
|
|
285
|
+
Phronomy.configuration.persistence ||
|
|
286
|
+
Phronomy::Persistence::InMemory.new
|
|
287
287
|
@agent_id = agent_id.to_s.freeze
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
288
|
+
|
|
289
|
+
if load_existing
|
|
290
|
+
root = records = nil
|
|
291
|
+
@persistence.transaction do |tx|
|
|
292
|
+
root = tx.agents.load(@agent_id)
|
|
293
|
+
records = tx.journals.read(
|
|
294
|
+
@agent_id,
|
|
295
|
+
limit: root.journal_position
|
|
296
|
+
)
|
|
297
297
|
end
|
|
298
|
-
|
|
298
|
+
validate_loaded_definition!(root)
|
|
299
|
+
@root = root
|
|
300
|
+
@_phronomy_journal_records = Array(records).dup.freeze
|
|
299
301
|
else
|
|
300
|
-
create_agent_root!(
|
|
302
|
+
@root = create_agent_root!(
|
|
303
|
+
context: context,
|
|
304
|
+
knowledge: knowledge,
|
|
305
|
+
metadata: metadata
|
|
306
|
+
)
|
|
307
|
+
@_phronomy_journal_records = @persistence.journals.read(
|
|
308
|
+
@agent_id,
|
|
309
|
+
limit: @root.journal_position
|
|
310
|
+
).dup.freeze
|
|
301
311
|
end
|
|
302
312
|
end
|
|
303
313
|
|
|
@@ -306,7 +316,10 @@ module Phronomy
|
|
|
306
316
|
end
|
|
307
317
|
|
|
308
318
|
def journal_projection
|
|
309
|
-
Agent::JournalProjection.new(
|
|
319
|
+
Agent::JournalProjection.new(
|
|
320
|
+
agent_root: @root,
|
|
321
|
+
records: _journal_records_snapshot
|
|
322
|
+
)
|
|
310
323
|
end
|
|
311
324
|
|
|
312
325
|
def transcript
|
|
@@ -334,13 +347,14 @@ module Phronomy
|
|
|
334
347
|
end
|
|
335
348
|
end
|
|
336
349
|
|
|
337
|
-
# Appends persistent Knowledge to the Agent Journal.
|
|
338
|
-
#
|
|
350
|
+
# Appends persistent Knowledge to the Agent Journal. The live Agent owns the
|
|
351
|
+
# current logical root/Journal view; Persistence is advanced optimistically.
|
|
339
352
|
def add_knowledge(content, metadata: {})
|
|
353
|
+
current = agent_root
|
|
340
354
|
next_root = nil
|
|
355
|
+
appended = nil
|
|
341
356
|
persistence.transaction do |tx|
|
|
342
357
|
tx.executions.assert_idle!(agent_id)
|
|
343
|
-
current = tx.agents.load(agent_id)
|
|
344
358
|
record = build_knowledge_record(
|
|
345
359
|
tx: tx,
|
|
346
360
|
root: current,
|
|
@@ -363,6 +377,7 @@ module Phronomy
|
|
|
363
377
|
root: next_root
|
|
364
378
|
)
|
|
365
379
|
end
|
|
380
|
+
_append_journal_records(appended)
|
|
366
381
|
@root = next_root
|
|
367
382
|
self
|
|
368
383
|
end
|
|
@@ -394,6 +409,7 @@ module Phronomy
|
|
|
394
409
|
tx.agents.delete(agent_id)
|
|
395
410
|
end
|
|
396
411
|
@root = nil
|
|
412
|
+
@_phronomy_journal_records = [].freeze
|
|
397
413
|
true
|
|
398
414
|
end
|
|
399
415
|
|
|
@@ -404,6 +420,17 @@ module Phronomy
|
|
|
404
420
|
|
|
405
421
|
private
|
|
406
422
|
|
|
423
|
+
def validate_loaded_definition!(loaded)
|
|
424
|
+
definition = self.class.agent_definition
|
|
425
|
+
return if loaded.agent_definition_id == definition.fetch(:id) &&
|
|
426
|
+
loaded.definition_version == definition.fetch(:version)
|
|
427
|
+
|
|
428
|
+
raise Phronomy::ConfigurationError,
|
|
429
|
+
"Agent definition mismatch for #{@agent_id}: stored " \
|
|
430
|
+
"#{loaded.agent_definition_id}@#{loaded.definition_version}, runtime " \
|
|
431
|
+
"#{definition.fetch(:id)}@#{definition.fetch(:version)}"
|
|
432
|
+
end
|
|
433
|
+
|
|
407
434
|
def create_agent_root!(context:, knowledge:, metadata:)
|
|
408
435
|
definition = self.class.agent_definition
|
|
409
436
|
root = Agent::AgentRoot.create(
|
|
@@ -480,10 +507,11 @@ module Phronomy
|
|
|
480
507
|
end
|
|
481
508
|
|
|
482
509
|
def mutate_context!(kind, context_affecting: true)
|
|
510
|
+
current = agent_root
|
|
483
511
|
next_root = nil
|
|
512
|
+
appended = nil
|
|
484
513
|
persistence.transaction do |tx|
|
|
485
514
|
tx.executions.assert_idle!(agent_id)
|
|
486
|
-
current = tx.agents.load(agent_id)
|
|
487
515
|
record = Agent::JournalRecord.new(
|
|
488
516
|
agent_id: agent_id,
|
|
489
517
|
kind: kind,
|
|
@@ -502,8 +530,13 @@ module Phronomy
|
|
|
502
530
|
context_revision: context_affecting ?
|
|
503
531
|
yield_context_revision(current, proposed) : current.context_revision
|
|
504
532
|
)
|
|
505
|
-
tx.agents.save(
|
|
533
|
+
tx.agents.save(
|
|
534
|
+
agent_id,
|
|
535
|
+
expected_revision: current.agent_revision,
|
|
536
|
+
root: next_root
|
|
537
|
+
)
|
|
506
538
|
end
|
|
539
|
+
_append_journal_records(appended)
|
|
507
540
|
@root = next_root
|
|
508
541
|
end
|
|
509
542
|
|
|
@@ -511,6 +544,18 @@ module Phronomy
|
|
|
511
544
|
(proposed.context_revision == current.context_revision) ? current.context_revision + 1 : proposed.context_revision
|
|
512
545
|
end
|
|
513
546
|
|
|
547
|
+
def _journal_records_snapshot
|
|
548
|
+
@_phronomy_journal_records || [].freeze
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def _append_journal_records(records)
|
|
552
|
+
incoming = Array(records)
|
|
553
|
+
return _journal_records_snapshot if incoming.empty?
|
|
554
|
+
|
|
555
|
+
@_phronomy_journal_records =
|
|
556
|
+
(_journal_records_snapshot + incoming).freeze
|
|
557
|
+
end
|
|
558
|
+
|
|
514
559
|
public
|
|
515
560
|
|
|
516
561
|
def _add_handoff_tool(tool_class)
|
|
@@ -14,18 +14,20 @@ module Phronomy
|
|
|
14
14
|
agent:,
|
|
15
15
|
persistence:,
|
|
16
16
|
policy: ContextPolicies::Default.new,
|
|
17
|
-
candidate_resolver: nil
|
|
17
|
+
candidate_resolver: nil,
|
|
18
|
+
journal_records: nil
|
|
18
19
|
)
|
|
19
20
|
@agent = agent
|
|
20
21
|
@persistence = persistence
|
|
21
22
|
@policy = policy
|
|
23
|
+
@journal_records = journal_records
|
|
22
24
|
@candidate_resolver = candidate_resolver || ContextCandidateResolver.new(
|
|
23
25
|
content_loader: method(:fetch_content)
|
|
24
26
|
)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def build_initial(input:, agent_root:, execution:, config: {}, patch: LLMInputPatch.empty)
|
|
28
|
-
projection =
|
|
30
|
+
projection = journal_projection(agent_root)
|
|
29
31
|
model_cfg = effective_model_config(config, patch)
|
|
30
32
|
tool_set = ToolDefinitionSet.build(@agent)
|
|
31
33
|
system_text = build_system_text(input)
|
|
@@ -65,7 +67,7 @@ module Phronomy
|
|
|
65
67
|
config: {},
|
|
66
68
|
patch: LLMInputPatch.empty
|
|
67
69
|
)
|
|
68
|
-
projection =
|
|
70
|
+
projection = journal_projection(agent_root)
|
|
69
71
|
model_cfg = effective_model_config(config, patch)
|
|
70
72
|
hook_candidates = normalize_candidates(patch.segment_candidates)
|
|
71
73
|
system_segments = base_manifest.segments.select do |segment|
|
|
@@ -96,6 +98,14 @@ module Phronomy
|
|
|
96
98
|
|
|
97
99
|
private
|
|
98
100
|
|
|
101
|
+
def journal_projection(agent_root)
|
|
102
|
+
if @journal_records
|
|
103
|
+
JournalProjection.new(agent_root: agent_root, records: @journal_records)
|
|
104
|
+
else
|
|
105
|
+
JournalProjection.new(persistence: @persistence, agent_root: agent_root)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
99
109
|
def assemble(
|
|
100
110
|
agent_root:,
|
|
101
111
|
execution:,
|