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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/CONTRIBUTING.md +30 -0
  4. data/README.md +2 -0
  5. data/docs/decisions/009-state-store-abstraction.md +1 -1
  6. data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
  7. data/docs/features.md +27 -1
  8. data/docs/getting-started.md +37 -1
  9. data/docs/migrations/0.19.md +154 -0
  10. data/docs/persistence-backends.md +504 -0
  11. data/docs/runtime-and-concurrency.md +93 -2
  12. data/lib/phronomy/agent/agent_execution.rb +29 -0
  13. data/lib/phronomy/agent/base.rb +81 -36
  14. data/lib/phronomy/agent/context_assembler.rb +13 -3
  15. data/lib/phronomy/agent/execution_coordinator.rb +420 -249
  16. data/lib/phronomy/agent/journal_projection.rb +5 -1
  17. data/lib/phronomy/agent/llm_call_record.rb +20 -0
  18. data/lib/phronomy/configuration.rb +2 -1
  19. data/lib/phronomy/engine/event_loop.rb +86 -8
  20. data/lib/phronomy/engine/fsm_session.rb +6 -4
  21. data/lib/phronomy/engine/runtime.rb +7 -0
  22. data/lib/phronomy/persistence/in_memory.rb +113 -8
  23. data/lib/phronomy/persistence.rb +109 -6
  24. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
  25. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
  26. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
  27. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
  28. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
  29. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
  30. data/lib/phronomy/testing/persistence_contract.rb +41 -0
  31. data/lib/phronomy/version.rb +1 -1
  32. data/lib/phronomy/workflow.rb +10 -9
  33. data/lib/phronomy/workflow_runner.rb +361 -95
  34. data/lib/phronomy.rb +9 -0
  35. metadata +12 -5
  36. data/lib/phronomy/state_store/base.rb +0 -48
  37. data/lib/phronomy/state_store/in_memory.rb +0 -62
  38. data/scripts/check_private_enforcement.rb +0 -93
@@ -252,24 +252,22 @@ module Phronomy
252
252
  new(agent_id: agent_id, persistence: persistence, load_existing: true)
253
253
  end
254
254
 
255
- def approve(execution_id, approval_request_id:, persistence:, approved: true, config: {})
256
- approve_async(
257
- execution_id,
258
- approval_request_id: approval_request_id,
259
- approved: approved,
260
- config: config,
261
- persistence: persistence
262
- ).wait_result
263
- end
264
-
265
- def approve_async(execution_id, approval_request_id:, persistence:, approved: true, config: {})
266
- execution = persistence.executions.load(execution_id)
267
- load(execution.agent_id, persistence: persistence).approve_async(
268
- execution_id,
269
- approval_request_id: approval_request_id,
270
- approved: approved,
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 || Phronomy::Persistence::InMemory.new
284
+ @persistence = persistence ||
285
+ Phronomy.configuration.persistence ||
286
+ Phronomy::Persistence::InMemory.new
287
287
  @agent_id = agent_id.to_s.freeze
288
- @root = if load_existing
289
- loaded = @persistence.agents.load(@agent_id)
290
- definition = self.class.agent_definition
291
- unless loaded.agent_definition_id == definition.fetch(:id) &&
292
- loaded.definition_version == definition.fetch(:version)
293
- raise Phronomy::ConfigurationError,
294
- "Agent definition mismatch for #{@agent_id}: stored " \
295
- "#{loaded.agent_definition_id}@#{loaded.definition_version}, runtime " \
296
- "#{definition.fetch(:id)}@#{definition.fetch(:version)}"
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
- loaded
298
+ validate_loaded_definition!(root)
299
+ @root = root
300
+ @_phronomy_journal_records = Array(records).dup.freeze
299
301
  else
300
- create_agent_root!(context: context, knowledge: knowledge, metadata: metadata)
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(persistence: persistence, agent_root: @root)
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
- # Knowledge is an optional Context candidate; it is not part of #transcript.
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(agent_id, expected_revision: current.agent_revision, root: next_root)
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 = JournalProjection.new(persistence: @persistence, agent_root: agent_root)
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 = JournalProjection.new(persistence: @persistence, agent_root: agent_root)
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:,