phronomy 0.16.0 → 0.18.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -4,8 +4,9 @@ module Phronomy
4
4
  module Agent
5
5
  # Builds FSMSession instances for AgentInvocation objects.
6
6
  #
7
- # Blocking/provider work returns through explicit Agent-internal events.
7
+ # Offloaded/provider work returns through explicit Agent-internal events.
8
8
  # Entry actions start operations and return synchronously.
9
+ # Every LLM Call is prepared from a canonical Manifest and RuntimeProjection.
9
10
  #
10
11
  # @api private
11
12
  class AgentInvocationSessionBuilder
@@ -40,7 +41,6 @@ module Phronomy
40
41
  def self.build(
41
42
  agent:,
42
43
  input:,
43
- messages:,
44
44
  config:,
45
45
  approval_policy: nil,
46
46
  approval_listener: nil,
@@ -51,7 +51,6 @@ module Phronomy
51
51
  invocation = AgentInvocation.new(
52
52
  agent: agent,
53
53
  input: input,
54
- messages: messages,
55
54
  config: config,
56
55
  approval_policy: approval_policy,
57
56
  approval_listener: approval_listener,
@@ -181,41 +180,20 @@ module Phronomy
181
180
  end
182
181
  private_class_method :build_entry_actions
183
182
 
184
- def self.filtering_input_action(agent, invocation)
185
- agent.send(
186
- :check_cancellation!,
187
- invocation.config,
188
- "invocation cancelled before input filtering"
189
- )
190
- invocation.input = agent.send(
191
- :run_input_filters!,
192
- invocation.input
193
- )
194
- invocation
195
- rescue Phronomy::FilterBlockError => error
196
- invocation.input_blocked = true
197
- invocation.block_error = error
183
+ def self.filtering_input_action(_agent, invocation)
184
+ invocation.input = invocation.config.fetch(:phronomy_filtered_input)
198
185
  invocation
199
186
  end
200
187
  private_class_method :filtering_input_action
201
188
 
202
189
  def self.building_context_action(agent, invocation)
203
- invocation.chat = agent.send(:build_chat)
204
- context = agent.send(
205
- :build_context,
206
- invocation.input,
207
- messages: invocation.messages,
208
- thread_id: invocation.thread_id,
209
- config: invocation.config,
210
- budget: agent.send(:build_token_budget),
211
- instruction: agent.send(:build_instructions, invocation.input),
212
- tools: agent.class.tools + agent.send(:_handoff_tools)
213
- )
214
- agent.send(:_apply_context_to_chat, invocation.chat, context)
190
+ projection = invocation.config.fetch(:phronomy_runtime_projection)
191
+ invocation.chat = agent.send(:build_chat, model_config: projection.model_config)
215
192
  agent.send(
216
- :run_before_completion_hooks!,
193
+ :_apply_runtime_projection_to_chat,
217
194
  invocation.chat,
218
- invocation.config
195
+ projection,
196
+ invocation: invocation
219
197
  )
220
198
  install_tool_interceptors(invocation.chat, invocation)
221
199
  invocation
@@ -270,23 +248,10 @@ module Phronomy
270
248
  private_class_method :build_tool_interception
271
249
 
272
250
  def self.calling_llm_action(agent, runtime, invocation)
273
- user_message = invocation.user_message_sent ?
274
- nil :
275
- agent.send(:extract_message, invocation.input)
276
- agent.send(
277
- :check_cancellation!,
278
- invocation.config,
279
- "invocation cancelled before LLM call"
280
- )
281
- operation = Phronomy.configuration.llm_adapter.complete_async(
282
- invocation.chat,
283
- user_message,
284
- config: invocation.config
285
- )
286
- observe_llm_operation(
287
- operation,
251
+ prepare_and_start_llm_call(
252
+ agent,
253
+ runtime,
288
254
  invocation,
289
- runtime: runtime,
290
255
  streaming: false
291
256
  )
292
257
  invocation
@@ -294,77 +259,175 @@ module Phronomy
294
259
  private_class_method :calling_llm_action
295
260
 
296
261
  def self.calling_llm_stream_action(agent, runtime, invocation)
297
- user_message = invocation.user_message_sent ?
298
- nil :
299
- agent.send(:extract_message, invocation.input)
262
+ prepare_and_start_llm_call(
263
+ agent,
264
+ runtime,
265
+ invocation,
266
+ streaming: true
267
+ )
268
+ invocation
269
+ end
270
+ private_class_method :calling_llm_stream_action
271
+
272
+ def self.prepare_and_start_llm_call(agent, runtime, invocation, streaming:)
273
+ activation = invocation.config.fetch(:phronomy_activation)
274
+ if invocation.user_message_sent
275
+ preparation = runtime.offload.submit(on_full: :raise) do
276
+ activation.coordinator.prepare_next_llm_call(activation)
277
+ end
278
+ preparation.on_complete do |projection, error|
279
+ if error
280
+ post_preparation_failure(runtime, invocation, error, streaming: streaming)
281
+ else
282
+ start_provider_call(
283
+ agent,
284
+ runtime,
285
+ invocation,
286
+ activation,
287
+ projection,
288
+ streaming: streaming,
289
+ replace_messages: true
290
+ )
291
+ end
292
+ end
293
+ else
294
+ start_provider_call(
295
+ agent,
296
+ runtime,
297
+ invocation,
298
+ activation,
299
+ activation.runtime_projection,
300
+ streaming: streaming,
301
+ replace_messages: false
302
+ )
303
+ end
304
+ end
305
+ private_class_method :prepare_and_start_llm_call
306
+
307
+ def self.start_provider_call(
308
+ agent,
309
+ runtime,
310
+ invocation,
311
+ activation,
312
+ projection,
313
+ streaming:,
314
+ replace_messages:
315
+ )
316
+ call_started = false
300
317
  agent.send(
301
318
  :check_cancellation!,
302
319
  invocation.config,
303
320
  "invocation cancelled before LLM call"
304
321
  )
305
-
306
- operation = Phronomy.configuration.llm_adapter.stream_async(
307
- invocation.chat,
308
- user_message,
309
- config: invocation.config
310
- ) do |chunk|
322
+ if replace_messages
323
+ invocation.chat = agent.send(:build_chat, model_config: projection.model_config)
311
324
  agent.send(
312
- :check_cancellation!,
313
- invocation.config,
314
- "invocation cancelled during streaming"
315
- )
316
- post_to_invocation!(
317
- runtime,
318
- invocation.id,
319
- :llm_stream_chunk,
320
- {content: chunk.content}
325
+ :_apply_runtime_projection_to_chat,
326
+ invocation.chat,
327
+ projection,
328
+ invocation: invocation
321
329
  )
330
+ install_tool_interceptors(invocation.chat, invocation)
331
+ invocation.config[:phronomy_runtime_projection] = projection
322
332
  end
323
333
 
324
- observe_llm_operation(
334
+ call_context = activation.begin_llm_call(projection)
335
+ call_started = true
336
+ invocation.begin_llm_call!(call_context.fetch(:llm_call_id))
337
+ message = projection.ask_message
338
+
339
+ operation = if streaming
340
+ Phronomy.configuration.llm_adapter.stream_async(
341
+ invocation.chat,
342
+ message,
343
+ config: invocation.config
344
+ ) do |chunk|
345
+ agent.send(
346
+ :check_cancellation!,
347
+ invocation.config,
348
+ "invocation cancelled during streaming"
349
+ )
350
+ post_to_invocation!(
351
+ runtime,
352
+ invocation.id,
353
+ :llm_stream_chunk,
354
+ {content: chunk.content}
355
+ )
356
+ end
357
+ else
358
+ Phronomy.configuration.llm_adapter.complete_async(
359
+ invocation.chat,
360
+ message,
361
+ config: invocation.config
362
+ )
363
+ end
364
+ observe_manifest_call(
325
365
  operation,
366
+ activation,
326
367
  invocation,
327
368
  runtime: runtime,
328
- streaming: true
369
+ streaming: streaming
329
370
  )
330
- invocation
371
+ rescue => error
372
+ if call_started
373
+ activation.record_llm_result(
374
+ response: canonical_response_for(nil, error),
375
+ error: error,
376
+ streaming: streaming
377
+ )
378
+ end
379
+ post_llm_result(runtime, invocation, nil, error, streaming: streaming)
331
380
  end
332
- private_class_method :calling_llm_stream_action
381
+ private_class_method :start_provider_call
333
382
 
334
- def self.observe_llm_operation(
335
- operation,
336
- invocation,
337
- runtime:,
338
- streaming:
339
- )
383
+ def self.observe_manifest_call(operation, activation, invocation, runtime:, streaming:)
340
384
  operation.on_complete do |response, error|
341
- result = LLMOperationResult.new(
342
- response: response,
385
+ activation.record_llm_result(
386
+ response: canonical_response_for(response, error),
343
387
  error: error,
344
388
  streaming: streaming
345
389
  )
346
- event_type =
347
- if error && !error.is_a?(ToolCallIntercepted)
348
- :llm_failed
349
- else
350
- :llm_completed
351
- end
352
- post_to_invocation!(
390
+ post_llm_result(
353
391
  runtime,
354
- invocation.id,
355
- event_type,
356
- result
392
+ invocation,
393
+ response,
394
+ error,
395
+ streaming: streaming
357
396
  )
358
397
  end
359
398
  end
360
- private_class_method :observe_llm_operation
399
+ private_class_method :observe_manifest_call
361
400
 
362
- def self.post_to_invocation!(
363
- runtime,
364
- invocation_id,
365
- event_type,
366
- payload
367
- )
401
+ def self.canonical_response_for(response, error)
402
+ if error.is_a?(ToolCallIntercepted)
403
+ error.assistant_outcome || ProviderCallOutcome.capture(response)
404
+ else
405
+ ProviderCallOutcome.capture(response)
406
+ end
407
+ end
408
+ private_class_method :canonical_response_for
409
+
410
+ def self.post_preparation_failure(runtime, invocation, error, streaming:)
411
+ post_llm_result(runtime, invocation, nil, error, streaming: streaming)
412
+ end
413
+ private_class_method :post_preparation_failure
414
+
415
+ def self.post_llm_result(runtime, invocation, response, error, streaming:)
416
+ result = LLMOperationResult.new(
417
+ response: response,
418
+ error: error,
419
+ streaming: streaming
420
+ )
421
+ event_type = if error && !error.is_a?(ToolCallIntercepted)
422
+ :llm_failed
423
+ else
424
+ :llm_completed
425
+ end
426
+ post_to_invocation!(runtime, invocation.id, event_type, result)
427
+ end
428
+ private_class_method :post_llm_result
429
+
430
+ def self.post_to_invocation!(runtime, invocation_id, event_type, payload)
368
431
  accepted = runtime.event_loop.post_to_session(
369
432
  Phronomy::Event.new(
370
433
  type: event_type,
@@ -9,7 +9,7 @@ module Phronomy
9
9
 
10
10
  ATTRIBUTES = %i[
11
11
  agent_id agent_definition_id definition_version agent_revision
12
- context_revision journal_position lifecycle_status transcript_generation memory_generation
12
+ context_revision journal_position lifecycle_status transcript_generation
13
13
  created_at updated_at metadata
14
14
  ].freeze
15
15
 
@@ -26,7 +26,6 @@ module Phronomy
26
26
  journal_position: 0,
27
27
  lifecycle_status: :idle,
28
28
  transcript_generation: 0,
29
- memory_generation: 0,
30
29
  created_at: now,
31
30
  updated_at: now,
32
31
  metadata: metadata
@@ -9,7 +9,7 @@ module Phronomy
9
9
  if invocation_context
10
10
  thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
11
11
  end
12
- _check_scheduler_reentrancy(:invoke, :invoke_async)
12
+ _check_event_loop_reentrancy(:invoke, :invoke_async)
13
13
  trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
14
14
  result = invoke_async(
15
15
  input,
@@ -58,7 +58,7 @@ module Phronomy
58
58
  if invocation_context
59
59
  thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
60
60
  end
61
- _check_scheduler_reentrancy(:stream, :stream_async)
61
+ _check_event_loop_reentrancy(:stream, :stream_async)
62
62
  trace("agent.stream", input: input, **_build_caller_meta(config)) do |_span|
63
63
  result = stream_async(
64
64
  input,
@@ -98,7 +98,7 @@ module Phronomy
98
98
  end
99
99
 
100
100
  def approve(execution_id, approval_request_id:, approved: true, config: {})
101
- _check_scheduler_reentrancy(:approve, :approve_async)
101
+ _check_event_loop_reentrancy(:approve, :approve_async)
102
102
  approve_async(
103
103
  execution_id,
104
104
  approval_request_id: approval_request_id,