phronomy 0.18.0 → 0.19.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.
@@ -26,10 +26,14 @@ module Phronomy
26
26
  if error
27
27
  post_prep_failure(runtime, on_event, result_task, translated(error))
28
28
  else
29
- register(prepared, result_task, mode: mode,
29
+ register(
30
+ prepared,
31
+ result_task,
32
+ mode: mode,
30
33
  approval_policy: approval_policy,
31
34
  approval_listener: approval_listener,
32
- on_event: on_event)
35
+ on_event: on_event
36
+ )
33
37
  end
34
38
  end
35
39
  result_task
@@ -52,27 +56,37 @@ module Phronomy
52
56
  name: "agent-approval-resume:#{execution_id}"
53
57
  )
54
58
  runtime = Phronomy::Runtime.instance
55
- preparation = runtime.offload.submit(on_full: :raise) do
56
- execution = @agent.persistence.executions.load(execution_id)
57
- unless execution.agent_id == @agent.agent_id && execution.status == :suspended
58
- raise ArgumentError,
59
- "execution is not a suspended execution of this agent: #{execution_id}"
60
- end
61
-
62
- activation = @agent.persistence.activations.fetch(execution_id)
63
- unless activation
64
- raise Phronomy::ExecutionRehydrationRequiredError,
59
+ activation = runtime.__agent_activations.fetch(execution_id)
60
+ unless activation
61
+ fail_task(
62
+ result_task,
63
+ Phronomy::ExecutionRehydrationRequiredError.new(
65
64
  "no live activation for #{execution_id}; durable rehydration is required"
66
- end
65
+ )
66
+ )
67
+ return result_task
68
+ end
67
69
 
70
+ unless activation.agent.equal?(@agent) &&
71
+ activation.execution.status == :suspended
72
+ fail_task(
73
+ result_task,
74
+ ArgumentError.new(
75
+ "execution is not a suspended execution of this agent: #{execution_id}"
76
+ )
77
+ )
78
+ return result_task
79
+ end
80
+
81
+ preparation = runtime.offload.submit(on_full: :raise) do
68
82
  mark_resuming!(
69
- execution_id,
83
+ activation,
70
84
  approval_request_id: approval_request_id,
71
85
  approved: approved
72
86
  )
73
87
  activation
74
88
  end
75
- preparation.on_complete do |activation, error|
89
+ preparation.on_complete do |prepared_activation, error|
76
90
  if error
77
91
  fail_task(result_task, translated(error))
78
92
  next
@@ -80,20 +94,24 @@ module Phronomy
80
94
 
81
95
  begin
82
96
  start_resume(
83
- activation,
97
+ prepared_activation,
84
98
  result_task,
85
99
  approved: approved,
86
100
  config: config
87
101
  )
88
102
  rescue => start_error
89
103
  commit = runtime.offload.submit(on_full: :raise) do
90
- commit_failed(activation, activation.invocation, start_error)
104
+ commit_failed(
105
+ prepared_activation,
106
+ prepared_activation.invocation,
107
+ start_error
108
+ )
91
109
  end
92
110
  commit.on_complete do |terminal, commit_error|
93
111
  if commit_error
94
112
  fail_task(result_task, commit_error)
95
113
  else
96
- @agent.persistence.activations.delete(execution_id)
114
+ runtime.__agent_activations.delete(execution_id)
97
115
  fail_task(result_task, terminal.fetch(:error))
98
116
  end
99
117
  end
@@ -101,22 +119,27 @@ module Phronomy
101
119
  end
102
120
  result_task
103
121
  rescue => error
104
- fail_task(result_task, translated(error))
122
+ fail_task(result_task, translated(error)) if result_task
105
123
  result_task
106
124
  end
107
125
 
108
- # Persists all Runtime events from the previous call, fixes the next
109
- # Canonical LLM Input Manifest, and materializes the next Runtime Projection.
110
- # This method is executed on an OffloadPool worker, never on EventLoop.
126
+ # Persists facts from the previous Provider/Tool cycle and fixes the next
127
+ # Manifest from the current local Agent state. This runs on OffloadPool.
111
128
  def prepare_next_llm_call(activation)
112
129
  snapshot = activation.runtime_snapshot
113
- manifest = manifest_ref = updated = root = nil
130
+ root = @agent.agent_root
131
+ current = activation.execution
132
+ journal_records = @agent.send(:_journal_records_snapshot)
133
+ manifest = manifest_ref = updated = nil
114
134
 
115
135
  @agent.persistence.transaction do |tx|
116
- root = tx.agents.load(@agent.agent_id)
117
- current = tx.executions.load(activation.execution_id)
136
+ assert_local_durable_base!(tx, root)
118
137
  encoded_records, call_records = encode_runtime_records(
119
- activation, tx: tx, snapshot: snapshot, context_candidate: true
138
+ activation,
139
+ tx: tx,
140
+ snapshot: snapshot,
141
+ context_candidate: true,
142
+ agent_root: root
120
143
  )
121
144
  staged = current.with(
122
145
  execution_revision: current.execution_revision,
@@ -131,28 +154,38 @@ module Phronomy
131
154
  config: invocation_config
132
155
  )
133
156
  manifest, manifest_ref = ContextAssembler.new(
134
- agent: @agent, persistence: tx, policy: context_policy_for(staged)
157
+ agent: @agent,
158
+ persistence: tx,
159
+ policy: context_policy_for(staged),
160
+ journal_records: journal_records
135
161
  ).build_followup(
136
162
  base_manifest: activation.base_manifest,
137
- agent_root: root, execution: staged,
138
- config: invocation_config, patch: patch
163
+ agent_root: root,
164
+ execution: staged,
165
+ config: invocation_config,
166
+ patch: patch
139
167
  )
140
168
  refs = Array(staged.metadata["manifest_refs"]) + [manifest_ref]
141
169
  updated = staged.with(
142
170
  phase: :calling_llm,
143
171
  metadata: staged.metadata.merge(
144
- "manifest_ref" => manifest_ref, "manifest_refs" => refs
172
+ "manifest_ref" => manifest_ref,
173
+ "manifest_refs" => refs
145
174
  )
146
175
  )
147
- tx.executions.save(current.execution_id,
148
- expected_revision: current.execution_revision, execution: updated)
176
+ tx.executions.save(
177
+ current.execution_id,
178
+ expected_revision: current.execution_revision,
179
+ execution: updated
180
+ )
149
181
  end
150
182
 
151
183
  activation.replace_execution(updated)
152
184
  activation.acknowledge_runtime_snapshot(snapshot)
153
185
 
154
186
  projection = RubyLLMMaterializer.new(
155
- agent: @agent, persistence: @agent.persistence
187
+ agent: @agent,
188
+ persistence: @agent.persistence
156
189
  ).materialize(manifest: manifest, manifest_ref: manifest_ref)
157
190
  activation.replace_runtime_projection(projection)
158
191
  projection
@@ -160,8 +193,12 @@ module Phronomy
160
193
 
161
194
  def prepare(input, thread_id:, config:)
162
195
  raw_message = @agent.send(:extract_message, input)
163
- execution, active_root = admit_execution(raw_message, thread_id: thread_id)
196
+ execution, active_root = admit_execution(
197
+ raw_message,
198
+ thread_id: thread_id
199
+ )
164
200
  @agent.__replace_root(active_root)
201
+ current_execution = execution
165
202
 
166
203
  begin
167
204
  effective_config = thread_id ? config.merge(thread_id: thread_id) : config
@@ -177,29 +214,27 @@ module Phronomy
177
214
  "invocation cancelled before context assembly"
178
215
  )
179
216
  filtered_message = @agent.send(:extract_message, filtered_input)
180
- manifest = nil
181
- manifest_ref = nil
182
- active_execution = nil
217
+ manifest = manifest_ref = active_execution = nil
218
+ local_journal = @agent.send(:_journal_records_snapshot)
183
219
 
184
220
  @agent.persistence.transaction do |tx|
185
- root = tx.agents.load(@agent.agent_id)
186
- current = tx.executions.load(execution.execution_id)
221
+ assert_local_durable_base!(tx, active_root)
187
222
  filtered_ref = tx.contents.put_text(filtered_message)
188
223
  input_record = JournalRecord.new(
189
224
  agent_id: @agent.agent_id,
190
- execution_id: current.execution_id,
225
+ execution_id: current_execution.execution_id,
191
226
  kind: :external_message,
192
227
  channel: :external,
193
228
  role: :user,
194
229
  content_ref: filtered_ref,
195
230
  correlation_id: thread_id,
196
- context_generation: root.transcript_generation,
231
+ context_generation: active_root.transcript_generation,
197
232
  context_candidate: true
198
233
  )
199
- staged = current.with(
200
- execution_revision: current.execution_revision,
201
- working_records: current.working_records + [input_record],
202
- metadata: current.metadata.merge(
234
+ staged = current_execution.with(
235
+ execution_revision: current_execution.execution_revision,
236
+ working_records: current_execution.working_records + [input_record],
237
+ metadata: current_execution.metadata.merge(
203
238
  "current_input_ref" => filtered_ref,
204
239
  "current_input_record_id" => input_record.record_id
205
240
  )
@@ -207,10 +242,11 @@ module Phronomy
207
242
  manifest, manifest_ref = ContextAssembler.new(
208
243
  agent: @agent,
209
244
  persistence: tx,
210
- policy: context_policy_for(staged)
245
+ policy: context_policy_for(staged),
246
+ journal_records: local_journal
211
247
  ).build_initial(
212
248
  input: filtered_input,
213
- agent_root: root,
249
+ agent_root: active_root,
214
250
  execution: staged,
215
251
  config: effective_config,
216
252
  patch: @agent.send(
@@ -229,34 +265,35 @@ module Phronomy
229
265
  )
230
266
  )
231
267
  tx.executions.save(
232
- current.execution_id,
233
- expected_revision: current.execution_revision,
268
+ current_execution.execution_id,
269
+ expected_revision: current_execution.execution_revision,
234
270
  execution: active_execution
235
271
  )
236
272
  end
273
+ current_execution = active_execution
237
274
 
238
275
  projection = RubyLLMMaterializer.new(
239
276
  agent: @agent,
240
277
  persistence: @agent.persistence
241
278
  ).materialize(manifest: manifest, manifest_ref: manifest_ref)
242
- Prepared.new(
279
+ Phronomy::Agent::ExecutionCoordinator::Prepared.new(
243
280
  execution: active_execution,
244
281
  runtime_projection: projection,
245
282
  filtered_input: filtered_input,
246
283
  config: effective_config
247
284
  )
248
285
  rescue => error
249
- commit_preparation_failure(execution, error)
286
+ commit_preparation_failure(current_execution, error)
250
287
  raise
251
288
  end
252
289
  end
253
290
 
254
291
  def admit_execution(raw_message, thread_id:)
255
- execution = nil
256
- root = nil
292
+ root = @agent.agent_root
293
+ raise Phronomy::Error, "agent is closed: #{@agent.agent_id}" if root.lifecycle_status == :closed
294
+
295
+ execution = next_root = nil
257
296
  @agent.persistence.transaction do |tx|
258
- root = tx.agents.load(@agent.agent_id)
259
- raise Phronomy::Error, "agent is closed: #{@agent.agent_id}" if root.lifecycle_status == :closed
260
297
  input_ref = tx.contents.put_text(raw_message)
261
298
  input_record = JournalRecord.new(
262
299
  agent_id: @agent.agent_id,
@@ -290,29 +327,34 @@ module Phronomy
290
327
  agent_revision: root.agent_revision + 1,
291
328
  lifecycle_status: :active
292
329
  )
293
- tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
294
- root = next_root
330
+ tx.agents.save(
331
+ root.agent_id,
332
+ expected_revision: root.agent_revision,
333
+ root: next_root
334
+ )
295
335
  end
296
- [execution, root]
336
+ [execution, next_root]
297
337
  end
298
338
 
299
339
  def commit_preparation_failure(execution, error)
300
340
  translated_error = translated(error)
301
- root = nil
341
+ root = @agent.agent_root
342
+ failed = next_root = appended = nil
343
+
302
344
  @agent.persistence.transaction do |tx|
303
- current = tx.executions.load(execution.execution_id)
304
- root = tx.agents.load(@agent.agent_id)
305
345
  error_ref = tx.contents.put_json(
306
346
  "class" => translated_error.class.name,
307
347
  "message" => translated_error.message
308
348
  )
309
- audit_records = current.working_records.map do |record|
310
- JournalRecord.from_h(record.to_h.merge("context_candidate" => false))
349
+ audit_records = execution.working_records.map do |record|
350
+ JournalRecord.from_h(
351
+ record.to_h.merge("context_candidate" => false)
352
+ )
311
353
  end
312
354
  terminal_status = terminal_status_for(translated_error)
313
355
  audit_records << JournalRecord.new(
314
356
  agent_id: @agent.agent_id,
315
- execution_id: current.execution_id,
357
+ execution_id: execution.execution_id,
316
358
  kind: execution_terminal_kind(terminal_status),
317
359
  channel: :audit,
318
360
  content_ref: error_ref,
@@ -324,7 +366,7 @@ module Phronomy
324
366
  expected_position: root.journal_position,
325
367
  records: audit_records
326
368
  )
327
- failed = current.with(
369
+ failed = execution.with(
328
370
  status: terminal_status,
329
371
  phase: terminal_status,
330
372
  working_records: [],
@@ -332,8 +374,8 @@ module Phronomy
332
374
  terminal_reason: translated_error.class.name
333
375
  )
334
376
  tx.executions.save(
335
- current.execution_id,
336
- expected_revision: current.execution_revision,
377
+ execution.execution_id,
378
+ expected_revision: execution.execution_revision,
337
379
  execution: failed
338
380
  )
339
381
  next_root = root.with(
@@ -341,10 +383,15 @@ module Phronomy
341
383
  journal_position: root.journal_position + appended.length,
342
384
  lifecycle_status: :idle
343
385
  )
344
- tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
345
- root = next_root
386
+ tx.agents.save(
387
+ root.agent_id,
388
+ expected_revision: root.agent_revision,
389
+ root: next_root
390
+ )
346
391
  end
347
- @agent.__replace_root(root)
392
+ @agent.send(:_append_journal_records, appended)
393
+ @agent.__replace_root(next_root)
394
+ failed
348
395
  end
349
396
 
350
397
  def register(prepared, result_task, mode:, approval_policy:, approval_listener:, on_event:)
@@ -355,8 +402,8 @@ module Phronomy
355
402
  coordinator: self,
356
403
  application_listener: on_event
357
404
  )
358
- @agent.persistence.activations.register(activation)
359
405
  runtime = Phronomy::Runtime.instance
406
+ runtime.__agent_activations.register(activation)
360
407
  event_loop = runtime.event_loop
361
408
  effective_config = prepared.config.merge(
362
409
  phronomy_activation: activation,
@@ -388,8 +435,10 @@ module Phronomy
388
435
  event_loop.register(session, completion: source_task)
389
436
  rescue => error
390
437
  activation ||= AgentExecutionActivation.new(
391
- execution: prepared.execution, agent: @agent,
392
- runtime_projection: prepared.runtime_projection, coordinator: self,
438
+ execution: prepared.execution,
439
+ agent: @agent,
440
+ runtime_projection: prepared.runtime_projection,
441
+ coordinator: self,
393
442
  application_listener: on_event
394
443
  )
395
444
  finish(activation, result_task, activation.invocation, error)
@@ -411,7 +460,11 @@ module Phronomy
411
460
  payload: {command: command}
412
461
  )
413
462
  )
414
- settle_without_listener(activation, result_task, outcome, commit_error) unless posted
463
+ unless posted
464
+ settle_without_listener(
465
+ activation, result_task, outcome, commit_error
466
+ )
467
+ end
415
468
  end
416
469
  rescue => caught
417
470
  fail_task(result_task, translated(caught))
@@ -431,8 +484,11 @@ module Phronomy
431
484
  )
432
485
  )
433
486
  settle_after_terminal(
434
- command.result_task, callback_error,
435
- terminal_event_type(command.error), nil, command.error
487
+ command.result_task,
488
+ callback_error,
489
+ terminal_event_type(command.error),
490
+ nil,
491
+ command.error
436
492
  )
437
493
  when AgentTerminalCommand
438
494
  deliver_execution_terminal(command)
@@ -445,7 +501,11 @@ module Phronomy
445
501
 
446
502
  def compute_terminal(activation, invocation, error)
447
503
  if (failure = activation.callback_failure)
448
- terminal = commit_failed(activation, invocation, failure.to_stream_callback_error)
504
+ terminal = commit_failed(
505
+ activation,
506
+ invocation,
507
+ failure.to_stream_callback_error
508
+ )
449
509
  return {type: :failed, error: terminal.fetch(:error)}
450
510
  end
451
511
  if error
@@ -453,10 +513,14 @@ module Phronomy
453
513
  return {type: :failed, error: terminal.fetch(:error)}
454
514
  end
455
515
  if invocation&.phase == :suspended
456
- return {type: :suspended, result: commit_suspended(activation, invocation)}
516
+ return {
517
+ type: :suspended,
518
+ result: commit_suspended(activation, invocation)
519
+ }
457
520
  end
458
521
  raise invocation.block_error if invocation.input_blocked? || invocation.output_blocked?
459
522
  raise invocation.error if invocation.error
523
+
460
524
  {type: :completed, result: commit_completed(activation, invocation)}
461
525
  rescue => caught
462
526
  terminal = commit_failed(activation, invocation, caught)
@@ -464,17 +528,20 @@ module Phronomy
464
528
  end
465
529
 
466
530
  def commit_suspended(activation, invocation)
467
- execution = activation.execution
531
+ current = activation.execution
532
+ root = @agent.agent_root
468
533
  request = invocation.approval_request
469
534
  runtime_snapshot = activation.runtime_snapshot
470
- suspended = nil
471
- root = nil
535
+ suspended = next_root = nil
536
+
472
537
  @agent.persistence.transaction do |tx|
473
538
  encoded_records, call_records = encode_runtime_records(
474
- activation, tx: tx, snapshot: runtime_snapshot, context_candidate: true
539
+ activation,
540
+ tx: tx,
541
+ snapshot: runtime_snapshot,
542
+ context_candidate: true,
543
+ agent_root: root
475
544
  )
476
- root = tx.agents.load(@agent.agent_id)
477
- current = tx.executions.load(execution.execution_id)
478
545
  request_ref = tx.contents.put_json(json_value(request.to_h))
479
546
  approval_record = JournalRecord.new(
480
547
  agent_id: @agent.agent_id,
@@ -502,33 +569,37 @@ module Phronomy
502
569
  agent_revision: root.agent_revision + 1,
503
570
  lifecycle_status: :suspended
504
571
  )
505
- tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
506
- root = next_root
572
+ tx.agents.save(
573
+ root.agent_id,
574
+ expected_revision: root.agent_revision,
575
+ root: next_root
576
+ )
507
577
  end
508
578
  activation.acknowledge_runtime_snapshot(runtime_snapshot)
509
579
  activation.replace_execution(suspended)
510
- @agent.__replace_root(root)
580
+ @agent.__replace_root(next_root)
511
581
  dispatch_approval_listener(invocation, request)
512
- result_base(suspended, root).merge(
582
+ result_base(suspended, next_root).merge(
513
583
  suspended: true,
514
584
  approval_request: request
515
585
  )
516
586
  end
517
587
 
518
588
  def commit_completed(activation, invocation)
519
- execution = activation.execution
589
+ current = activation.execution
590
+ root = @agent.agent_root
520
591
  runtime_snapshot = activation.runtime_snapshot
521
- completed = nil
522
- root = nil
523
- appended = nil
592
+ completed = next_root = appended = nil
524
593
 
525
594
  @agent.persistence.transaction do |tx|
526
595
  encoded_records, call_records = encode_runtime_records(
527
- activation, tx: tx, snapshot: runtime_snapshot, context_candidate: true
596
+ activation,
597
+ tx: tx,
598
+ snapshot: runtime_snapshot,
599
+ context_candidate: true,
600
+ agent_root: root
528
601
  )
529
602
  output_ref = tx.contents.put_text(invocation.output.to_s)
530
- root = tx.agents.load(@agent.agent_id)
531
- current = tx.executions.load(execution.execution_id)
532
603
  final_output_record = JournalRecord.new(
533
604
  agent_id: @agent.agent_id,
534
605
  execution_id: current.execution_id,
@@ -576,38 +647,47 @@ module Phronomy
576
647
  journal_position: root.journal_position + appended.length,
577
648
  lifecycle_status: :idle
578
649
  )
579
- tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
580
- root = next_root
650
+ tx.agents.save(
651
+ root.agent_id,
652
+ expected_revision: root.agent_revision,
653
+ root: next_root
654
+ )
581
655
  end
582
656
  activation.acknowledge_runtime_snapshot(runtime_snapshot)
583
657
  activation.replace_execution(completed)
584
- @agent.__replace_root(root)
585
- result_base(completed, root).merge(
658
+ @agent.send(:_append_journal_records, appended)
659
+ @agent.__replace_root(next_root)
660
+ result_base(completed, next_root).merge(
586
661
  output: invocation.output,
587
662
  rejected: invocation.rejected || nil,
588
663
  usage: invocation.usage,
589
- messages: transcript_messages(root)
664
+ messages: transcript_messages(next_root)
590
665
  ).compact
591
666
  end
592
667
 
593
668
  def commit_failed(activation, _invocation, error)
594
669
  translated_error = translated(error)
595
- execution = activation.execution
670
+ current = activation.execution
671
+ root = @agent.agent_root
596
672
  runtime_snapshot = activation.runtime_snapshot
597
- failed = nil
598
- root = nil
673
+ failed = next_root = appended = nil
674
+
599
675
  @agent.persistence.transaction do |tx|
600
676
  encoded_records, call_records = encode_runtime_records(
601
- activation, tx: tx, snapshot: runtime_snapshot, context_candidate: false
677
+ activation,
678
+ tx: tx,
679
+ snapshot: runtime_snapshot,
680
+ context_candidate: false,
681
+ agent_root: root
602
682
  )
603
683
  error_ref = tx.contents.put_json(
604
684
  "class" => translated_error.class.name,
605
685
  "message" => translated_error.message
606
686
  )
607
- root = tx.agents.load(@agent.agent_id)
608
- current = tx.executions.load(execution.execution_id)
609
687
  audit_records = (current.working_records + encoded_records).map do |record|
610
- JournalRecord.from_h(record.to_h.merge("context_candidate" => false))
688
+ JournalRecord.from_h(
689
+ record.to_h.merge("context_candidate" => false)
690
+ )
611
691
  end
612
692
  terminal_status = terminal_status_for(translated_error)
613
693
  audit_records << JournalRecord.new(
@@ -642,18 +722,28 @@ module Phronomy
642
722
  journal_position: root.journal_position + appended.length,
643
723
  lifecycle_status: :idle
644
724
  )
645
- tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
646
- root = next_root
725
+ tx.agents.save(
726
+ root.agent_id,
727
+ expected_revision: root.agent_revision,
728
+ root: next_root
729
+ )
647
730
  end
648
731
  activation.acknowledge_runtime_snapshot(runtime_snapshot)
649
732
  activation.replace_execution(failed)
650
- @agent.__replace_root(root)
651
- {error: translated_error, execution: failed, root: root}
733
+ @agent.send(:_append_journal_records, appended)
734
+ @agent.__replace_root(next_root)
735
+ {error: translated_error, execution: failed, root: next_root}
652
736
  end
653
737
 
654
- def encode_runtime_records(activation, tx:, snapshot:, context_candidate:)
738
+ def encode_runtime_records(
739
+ activation,
740
+ tx:,
741
+ snapshot:,
742
+ context_candidate:,
743
+ agent_root:
744
+ )
655
745
  execution = activation.execution
656
- root = tx.agents.load(@agent.agent_id)
746
+ root = agent_root
657
747
  records = []
658
748
  calls = []
659
749
 
@@ -665,10 +755,12 @@ module Phronomy
665
755
  call_error = intercepted ? nil : error
666
756
  output_ref = assistant_output_ref(tx, outcome)
667
757
  assistant_ref = assistant_message_ref(tx, outcome)
668
- error_ref = call_error ? tx.contents.put_json(
669
- "class" => call_error.class.name,
670
- "message" => call_error.message
671
- ) : nil
758
+ error_ref = if call_error
759
+ tx.contents.put_json(
760
+ "class" => call_error.class.name,
761
+ "message" => call_error.message
762
+ )
763
+ end
672
764
  usage_ref = if outcome && !outcome.usage.empty?
673
765
  tx.contents.put_json(json_value(outcome.usage))
674
766
  end
@@ -728,7 +820,9 @@ module Phronomy
728
820
  manifest_ref: active.fetch(:manifest_ref),
729
821
  started_at: active.fetch(:started_at),
730
822
  completed_at: Time.now.utc.iso8601(6),
731
- metadata: {"reason" => "execution_terminalized_before_provider_settlement"}
823
+ metadata: {
824
+ "reason" => "execution_terminalized_before_provider_settlement"
825
+ }
732
826
  )
733
827
  calls << abandoned
734
828
  records << JournalRecord.new(
@@ -746,18 +840,12 @@ module Phronomy
746
840
  snapshot.fetch(:runtime_events).each do |event|
747
841
  case event.type
748
842
  when :tool_call
749
- # Tool Calls remain part of the captured assistant message. The
750
- # StreamEvent is an application/runtime notification only.
751
843
  next
752
844
  when :tool_result
753
845
  payload = event.payload
754
846
  llm_call_id = payload[:llm_call_id] || payload["llm_call_id"]
755
847
  tool_call_id = payload.fetch(:tool_call_id).to_s
756
848
  tool_name = payload.fetch(:tool_name).to_s
757
-
758
- # The raw Tool return value is an execution fact, not an LLM message.
759
- # Keep it in the complete Journal, but never expose it directly as a
760
- # Context candidate.
761
849
  result_ref = put_runtime_content(tx, payload.fetch(:tool_result))
762
850
  records << JournalRecord.new(
763
851
  agent_id: @agent.agent_id,
@@ -775,8 +863,6 @@ module Phronomy
775
863
  }
776
864
  )
777
865
 
778
- # Separately preserve the exact Tool message that was appended to
779
- # RubyLLM Chat and therefore became eligible for a later Manifest.
780
866
  message_payload = if payload.key?(:tool_message)
781
867
  payload.fetch(:tool_message)
782
868
  else
@@ -803,56 +889,22 @@ module Phronomy
803
889
  [records, calls]
804
890
  end
805
891
 
806
- def assistant_message_ref(tx, outcome)
807
- return unless outcome
808
-
809
- payload = {
810
- "role" => "assistant",
811
- "content" => json_value(outcome.content),
812
- "tool_calls" => json_value(Array(outcome.tool_calls)),
813
- "model_id" => outcome.metadata["model_id"]
814
- }.compact
815
- tx.contents.put_json(payload)
816
- end
817
-
818
- def assistant_message_metadata(outcome)
819
- calls = Array(outcome&.tool_calls)
820
- {
821
- "tool_call_ids" => calls.map { |call| call.fetch("id").to_s },
822
- "tool_names" => calls.map { |call| call.fetch("name").to_s }
823
- }
824
- end
825
-
826
- def assistant_output_ref(tx, outcome)
827
- return unless outcome&.content_present?
828
-
829
- content = outcome.content
830
- text = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
831
- tx.contents.put_text(text)
832
- end
892
+ def mark_resuming!(activation, approval_request_id:, approved:)
893
+ current = activation.execution
894
+ current_root = @agent.agent_root
895
+ unless current.agent_id == @agent.agent_id && current.status == :suspended
896
+ raise ArgumentError, "execution is not suspended: #{current.execution_id}"
897
+ end
833
898
 
834
- def put_runtime_content(tx, value)
835
- value.is_a?(String) ? tx.contents.put_text(value) : tx.contents.put_json(json_value(value))
836
- end
899
+ request = current.approval_request || {}
900
+ request_id = request["id"] || request[:id]
901
+ unless request_id.to_s == approval_request_id.to_s
902
+ raise ArgumentError,
903
+ "approval request does not match execution #{current.execution_id}"
904
+ end
837
905
 
838
- def mark_resuming!(execution_id, approval_request_id:, approved:)
839
- root = nil
840
- updated = nil
906
+ updated = next_root = nil
841
907
  @agent.persistence.transaction do |tx|
842
- current = tx.executions.load(execution_id)
843
- unless current.agent_id == @agent.agent_id && current.status == :suspended
844
- raise ArgumentError,
845
- "execution is not suspended: #{execution_id}"
846
- end
847
-
848
- request = current.approval_request || {}
849
- request_id = request["id"] || request[:id]
850
- unless request_id.to_s == approval_request_id.to_s
851
- raise ArgumentError,
852
- "approval request does not match execution #{execution_id}"
853
- end
854
-
855
- current_root = tx.agents.load(@agent.agent_id)
856
908
  decision_ref = tx.contents.put_json(
857
909
  "approval_request_id" => request_id.to_s,
858
910
  "approved" => !!approved
@@ -887,13 +939,151 @@ module Phronomy
887
939
  expected_revision: current_root.agent_revision,
888
940
  root: next_root
889
941
  )
890
- root = next_root
891
942
  end
892
- @agent.persistence.activations.fetch(execution_id)&.replace_execution(updated)
893
- @agent.__replace_root(root)
943
+ activation.replace_execution(updated)
944
+ @agent.__replace_root(next_root)
894
945
  updated
895
946
  end
896
947
 
948
+ def assert_local_durable_base!(tx, root)
949
+ tx.assert_agent_watermark!(
950
+ agent_id: root.agent_id,
951
+ agent_revision: root.agent_revision,
952
+ journal_position: root.journal_position
953
+ )
954
+ end
955
+
956
+ def transcript_messages(root)
957
+ materializer = RubyLLMMaterializer.new(
958
+ agent: @agent,
959
+ persistence: @agent.persistence
960
+ )
961
+ materializer.materialize_journal_records(
962
+ JournalProjection.new(
963
+ agent_root: root,
964
+ records: @agent.send(:_journal_records_snapshot)
965
+ ).transcript_records
966
+ )
967
+ end
968
+
969
+ def deliver_execution_terminal(command)
970
+ activation = command.activation
971
+ registry = Phronomy::Runtime.instance.__agent_activations
972
+ if command.commit_error
973
+ callback_error = deliver_terminal(
974
+ activation,
975
+ :error,
976
+ error: command.commit_error
977
+ )
978
+ settle_after_terminal(
979
+ command.result_task,
980
+ callback_error,
981
+ :error,
982
+ nil,
983
+ command.commit_error
984
+ )
985
+ return
986
+ end
987
+
988
+ outcome = command.outcome
989
+ case outcome.fetch(:type)
990
+ when :suspended
991
+ result = outcome.fetch(:result)
992
+ callback_error = deliver_terminal(
993
+ activation,
994
+ :approval_required,
995
+ request: result.fetch(:approval_request)
996
+ )
997
+ settle_after_terminal(
998
+ command.result_task,
999
+ callback_error,
1000
+ :approval_required,
1001
+ result
1002
+ )
1003
+ when :completed
1004
+ registry.delete(activation.execution_id)
1005
+ result = outcome.fetch(:result)
1006
+ callback_error = deliver_terminal(activation, :done, result)
1007
+ settle_after_terminal(
1008
+ command.result_task,
1009
+ callback_error,
1010
+ :done,
1011
+ result
1012
+ )
1013
+ when :failed
1014
+ registry.delete(activation.execution_id)
1015
+ error = outcome.fetch(:error)
1016
+ type = terminal_event_type(error)
1017
+ callback_error = deliver_terminal(activation, type, error: error)
1018
+ settle_after_terminal(
1019
+ command.result_task,
1020
+ callback_error,
1021
+ type,
1022
+ nil,
1023
+ error
1024
+ )
1025
+ end
1026
+ end
1027
+
1028
+ def settle_without_listener(activation, result_task, outcome, commit_error)
1029
+ registry = Phronomy::Runtime.instance.__agent_activations
1030
+ if commit_error
1031
+ return fail_task(result_task, commit_error)
1032
+ end
1033
+
1034
+ case outcome&.fetch(:type)
1035
+ when :suspended
1036
+ complete_task(result_task, outcome.fetch(:result))
1037
+ when :completed
1038
+ registry.delete(activation.execution_id)
1039
+ complete_task(result_task, outcome.fetch(:result))
1040
+ when :failed
1041
+ registry.delete(activation.execution_id)
1042
+ fail_task(result_task, outcome.fetch(:error))
1043
+ else
1044
+ registry.delete(activation.execution_id)
1045
+ fail_task(
1046
+ result_task,
1047
+ Phronomy::RuntimeShutdownError.new(
1048
+ "EventLoop is not accepting terminal delivery"
1049
+ )
1050
+ )
1051
+ end
1052
+ end
1053
+
1054
+ def assistant_message_ref(tx, outcome)
1055
+ return unless outcome
1056
+
1057
+ payload = {
1058
+ "role" => "assistant",
1059
+ "content" => json_value(outcome.content),
1060
+ "tool_calls" => json_value(Array(outcome.tool_calls)),
1061
+ "model_id" => outcome.metadata["model_id"]
1062
+ }.compact
1063
+ tx.contents.put_json(payload)
1064
+ end
1065
+
1066
+ def assistant_message_metadata(outcome)
1067
+ calls = Array(outcome&.tool_calls)
1068
+ {
1069
+ "tool_call_ids" => calls.map { |call| call.fetch("id").to_s },
1070
+ "tool_names" => calls.map { |call| call.fetch("name").to_s }
1071
+ }
1072
+ end
1073
+
1074
+ def assistant_output_ref(tx, outcome)
1075
+ return unless outcome&.content_present?
1076
+
1077
+ content = outcome.content
1078
+ text = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
1079
+ tx.contents.put_text(text)
1080
+ end
1081
+
1082
+ def put_runtime_content(tx, value)
1083
+ value.is_a?(String) ?
1084
+ tx.contents.put_text(value) : tx.contents.put_json(json_value(value))
1085
+ end
1086
+
897
1087
  def start_resume(activation, result_task, approved:, config:)
898
1088
  invocation = activation.invocation
899
1089
  invocation.merge_config!(config)
@@ -909,7 +1099,12 @@ module Phronomy
909
1099
  activation.session = parent_session
910
1100
  source_task = Phronomy::Task.deferred(name: "#{result_task.name}-source")
911
1101
  source_task.on_complete do |completed, error|
912
- finish(activation, result_task, completed || parent_session.context, error)
1102
+ finish(
1103
+ activation,
1104
+ result_task,
1105
+ completed || parent_session.context,
1106
+ error
1107
+ )
913
1108
  end
914
1109
  event_loop.register(parent_session, completion: source_task)
915
1110
  invocation.tool_invocations.each do |child|
@@ -936,6 +1131,7 @@ module Phronomy
936
1131
  completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
937
1132
  completion.on_complete do |_result, error|
938
1133
  next unless error
1134
+
939
1135
  child.mark_framework_failed!(error)
940
1136
  runtime.event_loop.post_to_session(
941
1137
  Phronomy::Event.new(
@@ -966,23 +1162,15 @@ module Phronomy
966
1162
  ContextPolicyRegistry.default.resolve(descriptor)
967
1163
  end
968
1164
 
969
- def transcript_messages(root)
970
- materializer = RubyLLMMaterializer.new(
971
- agent: @agent,
972
- persistence: @agent.persistence
973
- )
974
- materializer.materialize_journal_records(
975
- JournalProjection.new(
976
- persistence: @agent.persistence,
977
- agent_root: root
978
- ).transcript_records
979
- )
980
- end
981
-
982
1165
  def deliver_terminal(activation, type, payload)
983
1166
  listener = activation.application_listener
984
1167
  return nil unless listener
985
- @agent.send(:_deliver_stream_event, listener, StreamEvent.new(type: type, payload: payload))
1168
+
1169
+ @agent.send(
1170
+ :_deliver_stream_event,
1171
+ listener,
1172
+ StreamEvent.new(type: type, payload: payload)
1173
+ )
986
1174
  end
987
1175
 
988
1176
  def post_prep_failure(runtime, listener, result_task, error)
@@ -997,79 +1185,55 @@ module Phronomy
997
1185
  fail_task(result_task, error) unless posted
998
1186
  end
999
1187
 
1000
- def deliver_execution_terminal(command)
1001
- activation = command.activation
1002
- if command.commit_error
1003
- callback_error = deliver_terminal(activation, :error, error: command.commit_error)
1004
- settle_after_terminal(command.result_task, callback_error, :error, nil, command.commit_error)
1005
- return
1006
- end
1007
- outcome = command.outcome
1008
- case outcome.fetch(:type)
1009
- when :suspended
1010
- result = outcome.fetch(:result)
1011
- callback_error = deliver_terminal(
1012
- activation, :approval_required,
1013
- request: result.fetch(:approval_request)
1014
- )
1015
- settle_after_terminal(command.result_task, callback_error, :approval_required, result)
1016
- when :completed
1017
- @agent.persistence.activations.delete(activation.execution_id)
1018
- result = outcome.fetch(:result)
1019
- callback_error = deliver_terminal(activation, :done, result)
1020
- settle_after_terminal(command.result_task, callback_error, :done, result)
1021
- when :failed
1022
- @agent.persistence.activations.delete(activation.execution_id)
1023
- error = outcome.fetch(:error)
1024
- type = terminal_event_type(error)
1025
- callback_error = deliver_terminal(activation, type, error: error)
1026
- settle_after_terminal(command.result_task, callback_error, type, nil, error)
1027
- end
1028
- end
1029
-
1030
- def settle_without_listener(activation, result_task, outcome, commit_error)
1031
- return fail_task(result_task, commit_error) if commit_error
1032
- case outcome&.fetch(:type)
1033
- when :suspended then complete_task(result_task, outcome.fetch(:result))
1034
- when :completed
1035
- @agent.persistence.activations.delete(activation.execution_id)
1036
- complete_task(result_task, outcome.fetch(:result))
1037
- when :failed
1038
- @agent.persistence.activations.delete(activation.execution_id)
1039
- fail_task(result_task, outcome.fetch(:error))
1040
- else
1041
- fail_task(result_task, Phronomy::RuntimeShutdownError.new("EventLoop is not accepting terminal delivery"))
1042
- end
1043
- end
1044
-
1045
- def settle_after_terminal(result_task, callback_error, event_type, result, execution_error = nil)
1188
+ def settle_after_terminal(
1189
+ result_task,
1190
+ callback_error,
1191
+ event_type,
1192
+ result,
1193
+ execution_error = nil
1194
+ )
1046
1195
  if callback_error
1047
1196
  policy = Phronomy.configuration.stream_callback_error_policy
1048
1197
  @agent.send(
1049
- :_report_stream_callback_error, callback_error,
1050
- event: StreamEvent.new(type: event_type, payload: result || {error: execution_error}),
1051
- invocation_id: nil, callback_error_policy: policy
1198
+ :_report_stream_callback_error,
1199
+ callback_error,
1200
+ event: StreamEvent.new(
1201
+ type: event_type,
1202
+ payload: result || {error: execution_error}
1203
+ ),
1204
+ invocation_id: nil,
1205
+ callback_error_policy: policy
1052
1206
  )
1053
1207
  if policy == :fail_task && execution_error.nil?
1054
- return fail_task(result_task, @agent.send(
1055
- :_build_stream_callback_error,
1056
- event_type: event_type, callback_error: callback_error, result: result
1057
- ))
1208
+ return fail_task(
1209
+ result_task,
1210
+ @agent.send(
1211
+ :_build_stream_callback_error,
1212
+ event_type: event_type,
1213
+ callback_error: callback_error,
1214
+ result: result
1215
+ )
1216
+ )
1058
1217
  end
1059
1218
  end
1060
- execution_error ? fail_task(result_task, execution_error) : complete_task(result_task, result)
1219
+ execution_error ?
1220
+ fail_task(result_task, execution_error) : complete_task(result_task, result)
1061
1221
  end
1062
1222
 
1063
1223
  def terminal_event_type(error)
1064
1224
  return :timeout if error.is_a?(Phronomy::TimeoutError)
1065
1225
  return :cancelled if error.is_a?(Phronomy::CancellationError)
1226
+
1066
1227
  :error
1067
1228
  end
1068
1229
 
1069
1230
  def dispatch_approval_listener(invocation, request)
1070
1231
  listener = invocation.approval_listener
1071
1232
  return unless listener
1072
- Phronomy::Runtime.instance.offload.submit(on_full: :raise) { listener.call(request) }
1233
+
1234
+ Phronomy::Runtime.instance.offload.submit(on_full: :raise) do
1235
+ listener.call(request)
1236
+ end
1073
1237
  end
1074
1238
 
1075
1239
  def json_value(value)
@@ -1086,14 +1250,21 @@ module Phronomy
1086
1250
  if value.respond_to?(:to_h)
1087
1251
  json_value(value.to_h)
1088
1252
  else
1089
- raise ArgumentError, "unsupported canonical runtime value: #{value.class}"
1253
+ raise ArgumentError,
1254
+ "unsupported canonical runtime value: #{value.class}"
1090
1255
  end
1091
1256
  end
1092
1257
  end
1093
1258
 
1094
1259
  def terminal_status_for(error)
1095
- return :cancelled if defined?(Phronomy::CancellationError) && error.is_a?(Phronomy::CancellationError)
1096
- return :blocked if defined?(Phronomy::FilterBlockError) && error.is_a?(Phronomy::FilterBlockError)
1260
+ if defined?(Phronomy::CancellationError) &&
1261
+ error.is_a?(Phronomy::CancellationError)
1262
+ return :cancelled
1263
+ end
1264
+ if defined?(Phronomy::FilterBlockError) &&
1265
+ error.is_a?(Phronomy::FilterBlockError)
1266
+ return :blocked
1267
+ end
1097
1268
 
1098
1269
  :failed
1099
1270
  end