phronomy 0.16.0 → 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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +54 -0
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +124 -92
  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 +1 -1
  10. data/benchmark/bench_tool_schema.rb +2 -35
  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 +2 -2
  14. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  15. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
  17. data/lib/phronomy/agent/agent_root.rb +1 -2
  18. data/lib/phronomy/agent/base.rb +135 -314
  19. data/lib/phronomy/agent/context/capability/base.rb +166 -297
  20. data/lib/phronomy/agent/context_assembler.rb +65 -29
  21. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  22. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  23. data/lib/phronomy/agent/execution_coordinator.rb +0 -1
  24. data/lib/phronomy/agent/journal_projection.rb +28 -2
  25. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  26. data/lib/phronomy/agent/shared_state.rb +46 -138
  27. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  28. data/lib/phronomy/agent/tool_invocation.rb +108 -314
  29. data/lib/phronomy/agent.rb +6 -10
  30. data/lib/phronomy/configuration.rb +15 -158
  31. data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
  32. data/lib/phronomy/engine/runtime.rb +15 -230
  33. data/lib/phronomy/engine/task_group.rb +30 -102
  34. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  35. data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
  36. data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
  37. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  38. data/lib/phronomy/version.rb +1 -1
  39. data/lib/phronomy.rb +3 -120
  40. data/scripts/api_snapshot.rb +1 -12
  41. metadata +3 -9
  42. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  43. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  44. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  45. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  46. data/lib/phronomy/knowledge_source.rb +0 -12
  47. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  48. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
@@ -6,6 +6,7 @@ module Phronomy
6
6
  #
7
7
  # Blocking/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.blocking_io.submit 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