phronomy 0.15.1 → 0.17.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +159 -28
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +400 -143
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +7 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +3 -19
  10. data/benchmark/bench_tool_schema.rb +2 -34
  11. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  12. data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
  13. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
  14. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
  15. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  16. data/lib/phronomy/agent/activation_registry.rb +28 -0
  17. data/lib/phronomy/agent/agent_execution.rb +97 -0
  18. data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
  19. data/lib/phronomy/agent/agent_invocation.rb +44 -46
  20. data/lib/phronomy/agent/agent_invocation_session_builder.rb +206 -104
  21. data/lib/phronomy/agent/agent_root.rb +66 -0
  22. data/lib/phronomy/agent/async_event_api.rb +55 -475
  23. data/lib/phronomy/agent/base.rb +351 -514
  24. data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
  25. data/lib/phronomy/agent/context/capability/base.rb +166 -297
  26. data/lib/phronomy/agent/context_assembler.rb +357 -0
  27. data/lib/phronomy/agent/context_candidate.rb +47 -0
  28. data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
  29. data/lib/phronomy/agent/context_importer.rb +217 -0
  30. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
  31. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
  32. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
  33. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +118 -0
  34. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
  35. data/lib/phronomy/agent/context_plan.rb +25 -0
  36. data/lib/phronomy/agent/context_plan_validator.rb +134 -0
  37. data/lib/phronomy/agent/context_policies/default.rb +53 -0
  38. data/lib/phronomy/agent/context_policy.rb +15 -0
  39. data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
  40. data/lib/phronomy/agent/context_policy_registry.rb +46 -0
  41. data/lib/phronomy/agent/context_request.rb +35 -0
  42. data/lib/phronomy/agent/context_selection_unit.rb +38 -0
  43. data/lib/phronomy/agent/derived_content_spec.rb +34 -0
  44. data/lib/phronomy/agent/execution_coordinator.rb +1122 -0
  45. data/lib/phronomy/agent/immutable.rb +31 -0
  46. data/lib/phronomy/agent/journal_projection.rb +60 -0
  47. data/lib/phronomy/agent/journal_record.rb +67 -0
  48. data/lib/phronomy/agent/llm_call_record.rb +51 -0
  49. data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
  50. data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
  51. data/lib/phronomy/agent/llm_input_patch.rb +21 -0
  52. data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
  53. data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
  54. data/lib/phronomy/agent/ruby_llm_materializer.rb +189 -0
  55. data/lib/phronomy/agent/shared_state.rb +46 -138
  56. data/lib/phronomy/agent/token_budget_resolver.rb +70 -0
  57. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
  58. data/lib/phronomy/agent/tool_definition_set.rb +55 -0
  59. data/lib/phronomy/agent/tool_invocation.rb +108 -314
  60. data/lib/phronomy/agent.rb +10 -16
  61. data/lib/phronomy/agent_busy_error.rb +5 -0
  62. data/lib/phronomy/canonical_json.rb +136 -0
  63. data/lib/phronomy/configuration.rb +17 -155
  64. data/lib/phronomy/content_store/base.rb +51 -0
  65. data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
  66. data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
  67. data/lib/phronomy/engine/event_loop.rb +3 -0
  68. data/lib/phronomy/engine/runtime.rb +15 -230
  69. data/lib/phronomy/engine/task_group.rb +30 -102
  70. data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
  71. data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
  72. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  73. data/lib/phronomy/multi_agent/orchestrator.rb +153 -204
  74. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
  75. data/lib/phronomy/multi_agent/team_coordinator.rb +46 -133
  76. data/lib/phronomy/persistence/in_memory.rb +247 -0
  77. data/lib/phronomy/persistence.rb +39 -0
  78. data/lib/phronomy/tools/agent.rb +14 -36
  79. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  80. data/lib/phronomy/version.rb +1 -1
  81. data/lib/phronomy.rb +9 -115
  82. data/scripts/add_to_h_to_token_doubles.rb +33 -0
  83. data/scripts/add_to_h_unnamed_doubles.rb +27 -0
  84. data/scripts/api_snapshot.rb +1 -12
  85. data/scripts/migrate_spec_agent_definition.rb +108 -0
  86. data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
  87. data/scripts/migrate_spec_inline_pass3.rb +24 -0
  88. metadata +54 -13
  89. data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
  90. data/lib/phronomy/agent/before_completion_context.rb +0 -47
  91. data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
  92. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  93. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  94. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  95. data/lib/phronomy/knowledge_source.rb +0 -12
  96. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  97. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
@@ -0,0 +1,1122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Phronomy
6
+ module Agent
7
+ class ExecutionCoordinator
8
+ Prepared = Data.define(:execution, :runtime_projection, :filtered_input, :config)
9
+ AgentTerminalCommand = Struct.new(:coordinator, :activation, :result_task, :outcome, :commit_error)
10
+ PrepFailureCommand = Struct.new(:coordinator, :on_event_listener, :result_task, :error)
11
+
12
+ def initialize(agent)
13
+ @agent = agent
14
+ end
15
+
16
+ def start(
17
+ input, thread_id: nil, config: {}, mode: :invoke,
18
+ approval_policy: nil, approval_listener: nil, on_event: nil
19
+ )
20
+ result_task = Phronomy::Task.deferred(name: "agent-#{@agent.agent_id}-#{mode}")
21
+ runtime = Phronomy::Runtime.instance
22
+ preparation = runtime.blocking_io.submit do
23
+ prepare(input, thread_id: thread_id, config: config)
24
+ end
25
+ preparation.on_complete do |prepared, error|
26
+ if error
27
+ post_prep_failure(runtime, on_event, result_task, translated(error))
28
+ else
29
+ register(prepared, result_task, mode: mode,
30
+ approval_policy: approval_policy,
31
+ approval_listener: approval_listener,
32
+ on_event: on_event)
33
+ end
34
+ end
35
+ result_task
36
+ rescue => error
37
+ if defined?(runtime) && runtime
38
+ post_prep_failure(runtime, on_event, result_task, translated(error))
39
+ else
40
+ fail_task(result_task, translated(error))
41
+ end
42
+ result_task
43
+ end
44
+
45
+ def resume(
46
+ execution_id,
47
+ approval_request_id:,
48
+ approved:,
49
+ config: {}
50
+ )
51
+ result_task = Phronomy::Task.deferred(
52
+ name: "agent-approval-resume:#{execution_id}"
53
+ )
54
+ runtime = Phronomy::Runtime.instance
55
+ preparation = runtime.blocking_io.submit 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,
65
+ "no live activation for #{execution_id}; durable rehydration is required"
66
+ end
67
+
68
+ mark_resuming!(
69
+ execution_id,
70
+ approval_request_id: approval_request_id,
71
+ approved: approved
72
+ )
73
+ activation
74
+ end
75
+ preparation.on_complete do |activation, error|
76
+ if error
77
+ fail_task(result_task, translated(error))
78
+ next
79
+ end
80
+
81
+ begin
82
+ start_resume(
83
+ activation,
84
+ result_task,
85
+ approved: approved,
86
+ config: config
87
+ )
88
+ rescue => start_error
89
+ commit = runtime.blocking_io.submit do
90
+ commit_failed(activation, activation.invocation, start_error)
91
+ end
92
+ commit.on_complete do |terminal, commit_error|
93
+ if commit_error
94
+ fail_task(result_task, commit_error)
95
+ else
96
+ @agent.persistence.activations.delete(execution_id)
97
+ fail_task(result_task, terminal.fetch(:error))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ result_task
103
+ rescue => error
104
+ fail_task(result_task, translated(error))
105
+ result_task
106
+ end
107
+
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 the blocking adapter pool, never on EventLoop.
111
+ def prepare_next_llm_call(activation)
112
+ snapshot = activation.runtime_snapshot
113
+ manifest = manifest_ref = updated = root = nil
114
+
115
+ @agent.persistence.transaction do |tx|
116
+ root = tx.agents.load(@agent.agent_id)
117
+ current = tx.executions.load(activation.execution_id)
118
+ encoded_records, call_records = encode_runtime_records(
119
+ activation, tx: tx, snapshot: snapshot, context_candidate: true
120
+ )
121
+ staged = current.with(
122
+ execution_revision: current.execution_revision,
123
+ phase: :preparing_llm_call,
124
+ working_records: current.working_records + encoded_records,
125
+ llm_calls: current.llm_calls + call_records
126
+ )
127
+ invocation_config = activation.invocation&.config || {}
128
+ patch = @agent.send(
129
+ :run_before_llm_input_hooks,
130
+ call_sequence: staged.llm_calls.length + 1,
131
+ config: invocation_config
132
+ )
133
+ manifest, manifest_ref = ContextAssembler.new(
134
+ agent: @agent, persistence: tx, policy: context_policy_for(staged)
135
+ ).build_followup(
136
+ base_manifest: activation.base_manifest,
137
+ agent_root: root, execution: staged,
138
+ config: invocation_config, patch: patch
139
+ )
140
+ refs = Array(staged.metadata["manifest_refs"]) + [manifest_ref]
141
+ updated = staged.with(
142
+ phase: :calling_llm,
143
+ metadata: staged.metadata.merge(
144
+ "manifest_ref" => manifest_ref, "manifest_refs" => refs
145
+ )
146
+ )
147
+ tx.executions.save(current.execution_id,
148
+ expected_revision: current.execution_revision, execution: updated)
149
+ end
150
+
151
+ activation.replace_execution(updated)
152
+ activation.acknowledge_runtime_snapshot(snapshot)
153
+
154
+ projection = RubyLLMMaterializer.new(
155
+ agent: @agent, persistence: @agent.persistence
156
+ ).materialize(manifest: manifest, manifest_ref: manifest_ref)
157
+ activation.replace_runtime_projection(projection)
158
+ projection
159
+ end
160
+
161
+ def prepare(input, thread_id:, config:)
162
+ raw_message = @agent.send(:extract_message, input)
163
+ execution, active_root = admit_execution(raw_message, thread_id: thread_id)
164
+ @agent.__replace_root(active_root)
165
+
166
+ begin
167
+ effective_config = thread_id ? config.merge(thread_id: thread_id) : config
168
+ @agent.send(
169
+ :check_cancellation!,
170
+ effective_config,
171
+ "invocation cancelled before input filtering"
172
+ )
173
+ filtered_input = @agent.send(:run_input_filters!, input)
174
+ @agent.send(
175
+ :check_cancellation!,
176
+ effective_config,
177
+ "invocation cancelled before context assembly"
178
+ )
179
+ filtered_message = @agent.send(:extract_message, filtered_input)
180
+ manifest = nil
181
+ manifest_ref = nil
182
+ active_execution = nil
183
+
184
+ @agent.persistence.transaction do |tx|
185
+ root = tx.agents.load(@agent.agent_id)
186
+ current = tx.executions.load(execution.execution_id)
187
+ filtered_ref = tx.contents.put_text(filtered_message)
188
+ input_record = JournalRecord.new(
189
+ agent_id: @agent.agent_id,
190
+ execution_id: current.execution_id,
191
+ kind: :external_message,
192
+ channel: :external,
193
+ role: :user,
194
+ content_ref: filtered_ref,
195
+ correlation_id: thread_id,
196
+ context_generation: root.transcript_generation,
197
+ context_candidate: true
198
+ )
199
+ staged = current.with(
200
+ execution_revision: current.execution_revision,
201
+ working_records: current.working_records + [input_record],
202
+ metadata: current.metadata.merge(
203
+ "current_input_ref" => filtered_ref,
204
+ "current_input_record_id" => input_record.record_id
205
+ )
206
+ )
207
+ manifest, manifest_ref = ContextAssembler.new(
208
+ agent: @agent,
209
+ persistence: tx,
210
+ policy: context_policy_for(staged)
211
+ ).build_initial(
212
+ input: filtered_input,
213
+ agent_root: root,
214
+ execution: staged,
215
+ config: effective_config,
216
+ patch: @agent.send(
217
+ :run_before_llm_input_hooks,
218
+ call_sequence: 1,
219
+ config: effective_config
220
+ )
221
+ )
222
+ active_execution = staged.with(
223
+ status: :active,
224
+ phase: :calling_llm,
225
+ metadata: staged.metadata.merge(
226
+ "base_manifest_ref" => manifest_ref,
227
+ "manifest_ref" => manifest_ref,
228
+ "manifest_refs" => [manifest_ref]
229
+ )
230
+ )
231
+ tx.executions.save(
232
+ current.execution_id,
233
+ expected_revision: current.execution_revision,
234
+ execution: active_execution
235
+ )
236
+ end
237
+
238
+ projection = RubyLLMMaterializer.new(
239
+ agent: @agent,
240
+ persistence: @agent.persistence
241
+ ).materialize(manifest: manifest, manifest_ref: manifest_ref)
242
+ Prepared.new(
243
+ execution: active_execution,
244
+ runtime_projection: projection,
245
+ filtered_input: filtered_input,
246
+ config: effective_config
247
+ )
248
+ rescue => error
249
+ commit_preparation_failure(execution, error)
250
+ raise
251
+ end
252
+ end
253
+
254
+ def admit_execution(raw_message, thread_id:)
255
+ execution = nil
256
+ root = nil
257
+ @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
+ input_ref = tx.contents.put_text(raw_message)
261
+ input_record = JournalRecord.new(
262
+ agent_id: @agent.agent_id,
263
+ kind: :input_received,
264
+ channel: :external,
265
+ role: :user,
266
+ content_ref: input_ref,
267
+ correlation_id: thread_id,
268
+ context_generation: root.transcript_generation,
269
+ context_candidate: false
270
+ )
271
+ policy_descriptor = ContextPolicies::Default.new.descriptor
272
+ execution = AgentExecution.start(
273
+ agent_root: root,
274
+ input_record: input_record,
275
+ metadata: {
276
+ "thread_id" => thread_id,
277
+ "current_input_ref" => input_ref,
278
+ "context_policy" => policy_descriptor.to_h
279
+ }.compact
280
+ )
281
+ input_record = JournalRecord.from_h(
282
+ input_record.to_h.merge("execution_id" => execution.execution_id)
283
+ )
284
+ execution = execution.with(
285
+ execution_revision: 0,
286
+ working_records: [input_record]
287
+ )
288
+ tx.executions.create_active(execution)
289
+ next_root = root.with(
290
+ agent_revision: root.agent_revision + 1,
291
+ lifecycle_status: :active
292
+ )
293
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
294
+ root = next_root
295
+ end
296
+ [execution, root]
297
+ end
298
+
299
+ def commit_preparation_failure(execution, error)
300
+ translated_error = translated(error)
301
+ root = nil
302
+ @agent.persistence.transaction do |tx|
303
+ current = tx.executions.load(execution.execution_id)
304
+ root = tx.agents.load(@agent.agent_id)
305
+ error_ref = tx.contents.put_json(
306
+ "class" => translated_error.class.name,
307
+ "message" => translated_error.message
308
+ )
309
+ audit_records = current.working_records.map do |record|
310
+ JournalRecord.from_h(record.to_h.merge("context_candidate" => false))
311
+ end
312
+ terminal_status = terminal_status_for(translated_error)
313
+ audit_records << JournalRecord.new(
314
+ agent_id: @agent.agent_id,
315
+ execution_id: current.execution_id,
316
+ kind: execution_terminal_kind(terminal_status),
317
+ channel: :audit,
318
+ content_ref: error_ref,
319
+ context_generation: root.transcript_generation,
320
+ context_candidate: false
321
+ )
322
+ appended = tx.journals.append(
323
+ root.agent_id,
324
+ expected_position: root.journal_position,
325
+ records: audit_records
326
+ )
327
+ failed = current.with(
328
+ status: terminal_status,
329
+ phase: terminal_status,
330
+ working_records: [],
331
+ error_ref: error_ref,
332
+ terminal_reason: translated_error.class.name
333
+ )
334
+ tx.executions.save(
335
+ current.execution_id,
336
+ expected_revision: current.execution_revision,
337
+ execution: failed
338
+ )
339
+ next_root = root.with(
340
+ agent_revision: root.agent_revision + 1,
341
+ journal_position: root.journal_position + appended.length,
342
+ lifecycle_status: :idle
343
+ )
344
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
345
+ root = next_root
346
+ end
347
+ @agent.__replace_root(root)
348
+ end
349
+
350
+ def register(prepared, result_task, mode:, approval_policy:, approval_listener:, on_event:)
351
+ activation = AgentExecutionActivation.new(
352
+ execution: prepared.execution,
353
+ agent: @agent,
354
+ runtime_projection: prepared.runtime_projection,
355
+ coordinator: self,
356
+ application_listener: on_event
357
+ )
358
+ @agent.persistence.activations.register(activation)
359
+ runtime = Phronomy::Runtime.instance
360
+ event_loop = runtime.event_loop
361
+ effective_config = prepared.config.merge(
362
+ phronomy_activation: activation,
363
+ phronomy_runtime_projection: prepared.runtime_projection,
364
+ phronomy_filtered_input: prepared.filtered_input,
365
+ execution_id: prepared.execution.execution_id
366
+ )
367
+ session = Agent::AgentInvocationSessionBuilder.build(
368
+ agent: @agent,
369
+ input: prepared.filtered_input,
370
+ config: effective_config,
371
+ approval_policy: approval_policy,
372
+ approval_listener: approval_listener,
373
+ mode: mode,
374
+ on_event: ->(event) { activation.record_event(event) },
375
+ runtime: runtime
376
+ )
377
+ activation.invocation = session.context
378
+ activation.session = session
379
+ source_task = Phronomy::Task.deferred(name: "#{result_task.name}-source")
380
+ source_task.on_complete do |invocation, error|
381
+ finish(
382
+ activation,
383
+ result_task,
384
+ invocation || session.context,
385
+ error
386
+ )
387
+ end
388
+ event_loop.register(session, completion: source_task)
389
+ rescue => error
390
+ activation ||= AgentExecutionActivation.new(
391
+ execution: prepared.execution, agent: @agent,
392
+ runtime_projection: prepared.runtime_projection, coordinator: self,
393
+ application_listener: on_event
394
+ )
395
+ finish(activation, result_task, activation.invocation, error)
396
+ end
397
+
398
+ def finish(activation, result_task, invocation, error)
399
+ runtime = Phronomy::Runtime.instance
400
+ operation = runtime.blocking_io.submit do
401
+ compute_terminal(activation, invocation, error)
402
+ end
403
+ operation.on_complete do |outcome, commit_error|
404
+ command = AgentTerminalCommand.new(
405
+ self, activation, result_task, outcome, commit_error
406
+ )
407
+ posted = runtime.event_loop.post(
408
+ Phronomy::Event.new(
409
+ type: :agent_terminal_ready,
410
+ target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
411
+ payload: {command: command}
412
+ )
413
+ )
414
+ settle_without_listener(activation, result_task, outcome, commit_error) unless posted
415
+ end
416
+ rescue => caught
417
+ fail_task(result_task, translated(caught))
418
+ end
419
+
420
+ public
421
+
422
+ def deliver_on_event_loop(command)
423
+ case command
424
+ when PrepFailureCommand
425
+ callback_error = @agent.send(
426
+ :_deliver_stream_event,
427
+ command.on_event_listener,
428
+ StreamEvent.new(
429
+ type: terminal_event_type(command.error),
430
+ payload: {error: command.error}
431
+ )
432
+ )
433
+ settle_after_terminal(
434
+ command.result_task, callback_error,
435
+ terminal_event_type(command.error), nil, command.error
436
+ )
437
+ when AgentTerminalCommand
438
+ deliver_execution_terminal(command)
439
+ else
440
+ raise Phronomy::Error, "unknown terminal command: #{command.class}"
441
+ end
442
+ end
443
+
444
+ private
445
+
446
+ def compute_terminal(activation, invocation, error)
447
+ if (failure = activation.callback_failure)
448
+ terminal = commit_failed(activation, invocation, failure.to_stream_callback_error)
449
+ return {type: :failed, error: terminal.fetch(:error)}
450
+ end
451
+ if error
452
+ terminal = commit_failed(activation, invocation, error)
453
+ return {type: :failed, error: terminal.fetch(:error)}
454
+ end
455
+ if invocation&.phase == :suspended
456
+ return {type: :suspended, result: commit_suspended(activation, invocation)}
457
+ end
458
+ raise invocation.block_error if invocation.input_blocked? || invocation.output_blocked?
459
+ raise invocation.error if invocation.error
460
+ {type: :completed, result: commit_completed(activation, invocation)}
461
+ rescue => caught
462
+ terminal = commit_failed(activation, invocation, caught)
463
+ {type: :failed, error: terminal.fetch(:error)}
464
+ end
465
+
466
+ def commit_suspended(activation, invocation)
467
+ execution = activation.execution
468
+ request = invocation.approval_request
469
+ runtime_snapshot = activation.runtime_snapshot
470
+ suspended = nil
471
+ root = nil
472
+ @agent.persistence.transaction do |tx|
473
+ encoded_records, call_records = encode_runtime_records(
474
+ activation, tx: tx, snapshot: runtime_snapshot, context_candidate: true
475
+ )
476
+ root = tx.agents.load(@agent.agent_id)
477
+ current = tx.executions.load(execution.execution_id)
478
+ request_ref = tx.contents.put_json(json_value(request.to_h))
479
+ approval_record = JournalRecord.new(
480
+ agent_id: @agent.agent_id,
481
+ execution_id: current.execution_id,
482
+ kind: :approval_required,
483
+ channel: :approval,
484
+ content_ref: request_ref,
485
+ correlation_id: request.id.to_s,
486
+ context_generation: root.transcript_generation,
487
+ context_candidate: false
488
+ )
489
+ suspended = current.with(
490
+ status: :suspended,
491
+ phase: :approval,
492
+ working_records: current.working_records + encoded_records + [approval_record],
493
+ llm_calls: current.llm_calls + call_records,
494
+ approval_request: json_value(request.to_h)
495
+ )
496
+ tx.executions.save(
497
+ current.execution_id,
498
+ expected_revision: current.execution_revision,
499
+ execution: suspended
500
+ )
501
+ next_root = root.with(
502
+ agent_revision: root.agent_revision + 1,
503
+ lifecycle_status: :suspended
504
+ )
505
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
506
+ root = next_root
507
+ end
508
+ activation.acknowledge_runtime_snapshot(runtime_snapshot)
509
+ activation.replace_execution(suspended)
510
+ @agent.__replace_root(root)
511
+ dispatch_approval_listener(invocation, request)
512
+ result_base(suspended, root).merge(
513
+ suspended: true,
514
+ approval_request: request
515
+ )
516
+ end
517
+
518
+ def commit_completed(activation, invocation)
519
+ execution = activation.execution
520
+ runtime_snapshot = activation.runtime_snapshot
521
+ completed = nil
522
+ root = nil
523
+ appended = nil
524
+
525
+ @agent.persistence.transaction do |tx|
526
+ encoded_records, call_records = encode_runtime_records(
527
+ activation, tx: tx, snapshot: runtime_snapshot, context_candidate: true
528
+ )
529
+ 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
+ final_output_record = JournalRecord.new(
533
+ agent_id: @agent.agent_id,
534
+ execution_id: current.execution_id,
535
+ kind: :final_output,
536
+ channel: :audit,
537
+ role: :assistant,
538
+ content_ref: output_ref,
539
+ context_generation: root.transcript_generation,
540
+ context_candidate: false
541
+ )
542
+ completed_record = JournalRecord.new(
543
+ agent_id: @agent.agent_id,
544
+ execution_id: current.execution_id,
545
+ kind: invocation.rejected ? :execution_rejected : :execution_completed,
546
+ channel: :audit,
547
+ content_ref: output_ref,
548
+ context_generation: root.transcript_generation,
549
+ context_candidate: false
550
+ )
551
+ all_records = current.working_records + encoded_records +
552
+ [final_output_record, completed_record]
553
+ appended = tx.journals.append(
554
+ root.agent_id,
555
+ expected_position: root.journal_position,
556
+ records: all_records
557
+ )
558
+ completed = current.with(
559
+ status: invocation.rejected ? :rejected : :completed,
560
+ phase: :completed,
561
+ working_records: [],
562
+ llm_calls: current.llm_calls + call_records,
563
+ approval_request: nil,
564
+ result_ref: output_ref,
565
+ terminal_reason: invocation.rejected ? "rejected" : "completed"
566
+ )
567
+ tx.executions.save(
568
+ current.execution_id,
569
+ expected_revision: current.execution_revision,
570
+ execution: completed
571
+ )
572
+ context_changed = appended.any?(&:context_candidate)
573
+ next_root = root.with(
574
+ agent_revision: root.agent_revision + 1,
575
+ context_revision: root.context_revision + (context_changed ? 1 : 0),
576
+ journal_position: root.journal_position + appended.length,
577
+ lifecycle_status: :idle
578
+ )
579
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
580
+ root = next_root
581
+ end
582
+ activation.acknowledge_runtime_snapshot(runtime_snapshot)
583
+ activation.replace_execution(completed)
584
+ @agent.__replace_root(root)
585
+ result_base(completed, root).merge(
586
+ output: invocation.output,
587
+ rejected: invocation.rejected || nil,
588
+ usage: invocation.usage,
589
+ messages: transcript_messages(root)
590
+ ).compact
591
+ end
592
+
593
+ def commit_failed(activation, _invocation, error)
594
+ translated_error = translated(error)
595
+ execution = activation.execution
596
+ runtime_snapshot = activation.runtime_snapshot
597
+ failed = nil
598
+ root = nil
599
+ @agent.persistence.transaction do |tx|
600
+ encoded_records, call_records = encode_runtime_records(
601
+ activation, tx: tx, snapshot: runtime_snapshot, context_candidate: false
602
+ )
603
+ error_ref = tx.contents.put_json(
604
+ "class" => translated_error.class.name,
605
+ "message" => translated_error.message
606
+ )
607
+ root = tx.agents.load(@agent.agent_id)
608
+ current = tx.executions.load(execution.execution_id)
609
+ audit_records = (current.working_records + encoded_records).map do |record|
610
+ JournalRecord.from_h(record.to_h.merge("context_candidate" => false))
611
+ end
612
+ terminal_status = terminal_status_for(translated_error)
613
+ audit_records << JournalRecord.new(
614
+ agent_id: @agent.agent_id,
615
+ execution_id: current.execution_id,
616
+ kind: execution_terminal_kind(terminal_status),
617
+ channel: :audit,
618
+ content_ref: error_ref,
619
+ context_generation: root.transcript_generation,
620
+ context_candidate: false
621
+ )
622
+ appended = tx.journals.append(
623
+ root.agent_id,
624
+ expected_position: root.journal_position,
625
+ records: audit_records
626
+ )
627
+ failed = current.with(
628
+ status: terminal_status,
629
+ phase: terminal_status,
630
+ working_records: [],
631
+ llm_calls: current.llm_calls + call_records,
632
+ error_ref: error_ref,
633
+ terminal_reason: translated_error.class.name
634
+ )
635
+ tx.executions.save(
636
+ current.execution_id,
637
+ expected_revision: current.execution_revision,
638
+ execution: failed
639
+ )
640
+ next_root = root.with(
641
+ agent_revision: root.agent_revision + 1,
642
+ journal_position: root.journal_position + appended.length,
643
+ lifecycle_status: :idle
644
+ )
645
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
646
+ root = next_root
647
+ end
648
+ activation.acknowledge_runtime_snapshot(runtime_snapshot)
649
+ activation.replace_execution(failed)
650
+ @agent.__replace_root(root)
651
+ {error: translated_error, execution: failed, root: root}
652
+ end
653
+
654
+ def encode_runtime_records(activation, tx:, snapshot:, context_candidate:)
655
+ execution = activation.execution
656
+ root = tx.agents.load(@agent.agent_id)
657
+ records = []
658
+ calls = []
659
+
660
+ snapshot.fetch(:llm_results).each_with_index do |item, index|
661
+ outcome = item[:response]
662
+ error = item[:error]
663
+ llm_call_id = item.fetch(:llm_call_id).to_s
664
+ intercepted = error.is_a?(ToolCallIntercepted)
665
+ call_error = intercepted ? nil : error
666
+ output_ref = assistant_output_ref(tx, outcome)
667
+ 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
672
+ usage_ref = if outcome && !outcome.usage.empty?
673
+ tx.contents.put_json(json_value(outcome.usage))
674
+ end
675
+ call = LLMCallRecord.new(
676
+ llm_call_id: llm_call_id,
677
+ execution_id: execution.execution_id,
678
+ sequence: execution.llm_calls.length + index + 1,
679
+ status: call_error ? :failed : :completed,
680
+ manifest_ref: item.fetch(:manifest_ref),
681
+ output_ref: output_ref,
682
+ error_ref: error_ref,
683
+ usage_ref: usage_ref,
684
+ started_at: item.fetch(:started_at),
685
+ completed_at: Time.now.utc.iso8601(6),
686
+ metadata: {
687
+ "streaming" => item[:streaming],
688
+ "tool_call_intercepted" => intercepted,
689
+ "assistant_outcome_captured" => !outcome.nil?,
690
+ "tool_call_count" => outcome ? outcome.tool_calls.length : 0
691
+ }
692
+ )
693
+ calls << call
694
+ call_ref = tx.contents.put_json(call.to_h)
695
+ records << JournalRecord.new(
696
+ agent_id: @agent.agent_id,
697
+ execution_id: execution.execution_id,
698
+ llm_call_id: llm_call_id,
699
+ kind: :llm_call_recorded,
700
+ channel: :audit,
701
+ content_ref: call_ref,
702
+ context_generation: root.transcript_generation,
703
+ context_candidate: false
704
+ )
705
+
706
+ if assistant_ref
707
+ records << JournalRecord.new(
708
+ agent_id: @agent.agent_id,
709
+ execution_id: execution.execution_id,
710
+ llm_call_id: llm_call_id,
711
+ kind: :assistant_message,
712
+ channel: :llm,
713
+ role: :assistant,
714
+ content_ref: assistant_ref,
715
+ context_generation: root.transcript_generation,
716
+ context_candidate: context_candidate,
717
+ metadata: assistant_message_metadata(outcome)
718
+ )
719
+ end
720
+ end
721
+
722
+ if (active = snapshot[:active_call])
723
+ abandoned = LLMCallRecord.new(
724
+ llm_call_id: active.fetch(:llm_call_id),
725
+ execution_id: execution.execution_id,
726
+ sequence: execution.llm_calls.length + calls.length + 1,
727
+ status: :cancelled,
728
+ manifest_ref: active.fetch(:manifest_ref),
729
+ started_at: active.fetch(:started_at),
730
+ completed_at: Time.now.utc.iso8601(6),
731
+ metadata: {"reason" => "execution_terminalized_before_provider_settlement"}
732
+ )
733
+ calls << abandoned
734
+ records << JournalRecord.new(
735
+ agent_id: @agent.agent_id,
736
+ execution_id: execution.execution_id,
737
+ llm_call_id: abandoned.llm_call_id,
738
+ kind: :llm_call_recorded,
739
+ channel: :audit,
740
+ content_ref: tx.contents.put_json(abandoned.to_h),
741
+ context_generation: root.transcript_generation,
742
+ context_candidate: false
743
+ )
744
+ end
745
+
746
+ snapshot.fetch(:runtime_events).each do |event|
747
+ case event.type
748
+ when :tool_call
749
+ # Tool Calls remain part of the captured assistant message. The
750
+ # StreamEvent is an application/runtime notification only.
751
+ next
752
+ when :tool_result
753
+ payload = event.payload
754
+ llm_call_id = payload[:llm_call_id] || payload["llm_call_id"]
755
+ tool_call_id = payload.fetch(:tool_call_id).to_s
756
+ 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
+ result_ref = put_runtime_content(tx, payload.fetch(:tool_result))
762
+ records << JournalRecord.new(
763
+ agent_id: @agent.agent_id,
764
+ execution_id: execution.execution_id,
765
+ llm_call_id: llm_call_id,
766
+ kind: :tool_result,
767
+ channel: :tool,
768
+ content_ref: result_ref,
769
+ causation_id: tool_call_id,
770
+ context_generation: root.transcript_generation,
771
+ context_candidate: false,
772
+ metadata: {
773
+ "tool_call_id" => tool_call_id,
774
+ "tool_name" => tool_name
775
+ }
776
+ )
777
+
778
+ # Separately preserve the exact Tool message that was appended to
779
+ # RubyLLM Chat and therefore became eligible for a later Manifest.
780
+ message_payload = if payload.key?(:tool_message)
781
+ payload.fetch(:tool_message)
782
+ else
783
+ payload.fetch("tool_message")
784
+ end
785
+ records << JournalRecord.new(
786
+ agent_id: @agent.agent_id,
787
+ execution_id: execution.execution_id,
788
+ llm_call_id: llm_call_id,
789
+ kind: :tool_message,
790
+ channel: :tool,
791
+ role: :tool,
792
+ content_ref: tx.contents.put_json(json_value(message_payload)),
793
+ causation_id: tool_call_id,
794
+ context_generation: root.transcript_generation,
795
+ context_candidate: context_candidate,
796
+ metadata: {
797
+ "tool_call_id" => tool_call_id,
798
+ "tool_name" => tool_name
799
+ }
800
+ )
801
+ end
802
+ end
803
+ [records, calls]
804
+ end
805
+
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
833
+
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
837
+
838
+ def mark_resuming!(execution_id, approval_request_id:, approved:)
839
+ root = nil
840
+ updated = nil
841
+ @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
+ decision_ref = tx.contents.put_json(
857
+ "approval_request_id" => request_id.to_s,
858
+ "approved" => !!approved
859
+ )
860
+ decision_record = JournalRecord.new(
861
+ agent_id: @agent.agent_id,
862
+ execution_id: current.execution_id,
863
+ kind: :approval_decided,
864
+ channel: :approval,
865
+ content_ref: decision_ref,
866
+ correlation_id: request_id.to_s,
867
+ context_generation: current_root.transcript_generation,
868
+ context_candidate: false
869
+ )
870
+ updated = current.with(
871
+ status: :active,
872
+ phase: :resuming,
873
+ working_records: current.working_records + [decision_record],
874
+ approval_request: request.merge("approved" => approved)
875
+ )
876
+ tx.executions.save(
877
+ current.execution_id,
878
+ expected_revision: current.execution_revision,
879
+ execution: updated
880
+ )
881
+ next_root = current_root.with(
882
+ agent_revision: current_root.agent_revision + 1,
883
+ lifecycle_status: :active
884
+ )
885
+ tx.agents.save(
886
+ current_root.agent_id,
887
+ expected_revision: current_root.agent_revision,
888
+ root: next_root
889
+ )
890
+ root = next_root
891
+ end
892
+ @agent.persistence.activations.fetch(execution_id)&.replace_execution(updated)
893
+ @agent.__replace_root(root)
894
+ updated
895
+ end
896
+
897
+ def start_resume(activation, result_task, approved:, config:)
898
+ invocation = activation.invocation
899
+ invocation.merge_config!(config)
900
+ invocation.begin_approval_resume!(approved: approved)
901
+ runtime = Phronomy::Runtime.instance
902
+ event_loop = runtime.event_loop
903
+ parent_session = Agent::AgentInvocationSessionBuilder.build_for_resume(
904
+ agent_invocation: invocation,
905
+ resume_event: :resume,
906
+ resume_phase: :suspended,
907
+ runtime: runtime
908
+ )
909
+ activation.session = parent_session
910
+ source_task = Phronomy::Task.deferred(name: "#{result_task.name}-source")
911
+ source_task.on_complete do |completed, error|
912
+ finish(activation, result_task, completed || parent_session.context, error)
913
+ end
914
+ event_loop.register(parent_session, completion: source_task)
915
+ invocation.tool_invocations.each do |child|
916
+ child_session = if child.awaiting_approval?
917
+ Agent::ToolInvocationSessionBuilder.build_for_resume(
918
+ tool_invocation: child,
919
+ resume_event: approved ? :approve : :reject,
920
+ resume_phase: :awaiting_approval,
921
+ runtime: runtime
922
+ )
923
+ elsif !approved && child.authorized?
924
+ Agent::ToolInvocationSessionBuilder.build_for_resume(
925
+ tool_invocation: child,
926
+ resume_event: :cancel,
927
+ resume_phase: :authorized,
928
+ runtime: runtime
929
+ )
930
+ end
931
+ register_child(event_loop, runtime, child, child_session) if child_session
932
+ end
933
+ end
934
+
935
+ def register_child(event_loop, runtime, child, session)
936
+ completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
937
+ completion.on_complete do |_result, error|
938
+ next unless error
939
+ child.mark_framework_failed!(error)
940
+ runtime.event_loop.post_to_session(
941
+ Phronomy::Event.new(
942
+ type: :tool_failed,
943
+ target_id: child.parent_agent_invocation_id,
944
+ payload: {tool_invocation_id: child.id}
945
+ )
946
+ )
947
+ end
948
+ event_loop.register(session, completion: completion)
949
+ end
950
+
951
+ def result_base(execution, root)
952
+ {
953
+ agent_id: root.agent_id,
954
+ execution_id: execution.execution_id,
955
+ agent_revision: root.agent_revision,
956
+ context_revision: root.context_revision,
957
+ journal_position: root.journal_position
958
+ }
959
+ end
960
+
961
+ def context_policy_for(execution)
962
+ descriptor_hash = execution.metadata.fetch("context_policy") do
963
+ ContextPolicies::Default.new.descriptor.to_h
964
+ end
965
+ descriptor = ContextPolicyDescriptor.from_h(descriptor_hash)
966
+ ContextPolicyRegistry.default.resolve(descriptor)
967
+ end
968
+
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
+ def deliver_terminal(activation, type, payload)
983
+ listener = activation.application_listener
984
+ return nil unless listener
985
+ @agent.send(:_deliver_stream_event, listener, StreamEvent.new(type: type, payload: payload))
986
+ end
987
+
988
+ def post_prep_failure(runtime, listener, result_task, error)
989
+ command = PrepFailureCommand.new(self, listener, result_task, error)
990
+ posted = runtime.event_loop.post(
991
+ Phronomy::Event.new(
992
+ type: :agent_terminal_ready,
993
+ target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
994
+ payload: {command: command}
995
+ )
996
+ )
997
+ fail_task(result_task, error) unless posted
998
+ end
999
+
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)
1046
+ if callback_error
1047
+ policy = Phronomy.configuration.stream_callback_error_policy
1048
+ @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
1052
+ )
1053
+ 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
+ ))
1058
+ end
1059
+ end
1060
+ execution_error ? fail_task(result_task, execution_error) : complete_task(result_task, result)
1061
+ end
1062
+
1063
+ def terminal_event_type(error)
1064
+ return :timeout if error.is_a?(Phronomy::TimeoutError)
1065
+ return :cancelled if error.is_a?(Phronomy::CancellationError)
1066
+ :error
1067
+ end
1068
+
1069
+ def dispatch_approval_listener(invocation, request)
1070
+ listener = invocation.approval_listener
1071
+ return unless listener
1072
+ Phronomy::Runtime.instance.blocking_io.submit { listener.call(request) }
1073
+ end
1074
+
1075
+ def json_value(value)
1076
+ case value
1077
+ when Hash
1078
+ value.to_h { |key, child| [key.to_s, json_value(child)] }
1079
+ when Array
1080
+ value.map { |child| json_value(child) }
1081
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
1082
+ value
1083
+ when Symbol
1084
+ value.to_s
1085
+ else
1086
+ if value.respond_to?(:to_h)
1087
+ json_value(value.to_h)
1088
+ else
1089
+ raise ArgumentError, "unsupported canonical runtime value: #{value.class}"
1090
+ end
1091
+ end
1092
+ end
1093
+
1094
+ 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)
1097
+
1098
+ :failed
1099
+ end
1100
+
1101
+ def execution_terminal_kind(status)
1102
+ {
1103
+ cancelled: :execution_cancelled,
1104
+ blocked: :execution_blocked,
1105
+ failed: :execution_failed
1106
+ }.fetch(status)
1107
+ end
1108
+
1109
+ def translated(error)
1110
+ @agent.send(:_translated_error, error)
1111
+ end
1112
+
1113
+ def complete_task(task, result)
1114
+ @agent.send(:_complete_result_task, task, result)
1115
+ end
1116
+
1117
+ def fail_task(task, error)
1118
+ @agent.send(:_fail_result_task, task, error)
1119
+ end
1120
+ end
1121
+ end
1122
+ end