runtype 0.1.0 → 0.2.1

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 (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +92 -6
  4. data/generated/lib/runtype/agent_aliases/client.rb +320 -0
  5. data/generated/lib/runtype/agent_aliases/types/delete_v1agents_id_aliases_alias_request.rb +17 -0
  6. data/generated/lib/runtype/agent_aliases/types/delete_v1agents_id_aliases_alias_response.rb +17 -0
  7. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_alias_request.rb +15 -0
  8. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_alias_response.rb +29 -0
  9. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_request.rb +17 -0
  10. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_response.rb +13 -0
  11. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_response_data_item.rb +29 -0
  12. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_request.rb +17 -0
  13. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_response.rb +13 -0
  14. data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_response_data_item.rb +29 -0
  15. data/generated/lib/runtype/agent_aliases/types/post_v1agents_id_aliases_alias_rollback_request.rb +21 -0
  16. data/generated/lib/runtype/agent_aliases/types/post_v1agents_id_aliases_alias_rollback_response.rb +21 -0
  17. data/generated/lib/runtype/agent_aliases/types/put_v1agents_id_aliases_alias_request.rb +21 -0
  18. data/generated/lib/runtype/agent_aliases/types/put_v1agents_id_aliases_alias_response.rb +21 -0
  19. data/generated/lib/runtype/agents/client.rb +14 -2
  20. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response.rb +6 -0
  21. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response_agent_target_resolution.rb +15 -0
  22. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response_agent_version.rb +17 -0
  23. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item.rb +6 -0
  24. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item_agent_target_resolution.rb +15 -0
  25. data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item_agent_version.rb +17 -0
  26. data/generated/lib/runtype/agents/types/get_v1agents_id_response.rb +2 -0
  27. data/generated/lib/runtype/agents/types/get_v1agents_id_response_sandbox_capability_issues_item.rb +19 -0
  28. data/generated/lib/runtype/agents/types/get_v1agents_id_response_sandbox_capability_issues_item_code.rb +13 -0
  29. data/generated/lib/runtype/agents/types/get_v1agents_request.rb +2 -0
  30. data/generated/lib/runtype/agents/types/get_v1agents_request_managed_in_code.rb +14 -0
  31. data/generated/lib/runtype/agents/types/get_v1agents_response_data_item.rb +4 -0
  32. data/generated/lib/runtype/agents/types/get_v1agents_response_data_item_sandbox_capability_issues_item.rb +19 -0
  33. data/generated/lib/runtype/agents/types/get_v1agents_response_data_item_sandbox_capability_issues_item_code.rb +13 -0
  34. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request.rb +4 -0
  35. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config.rb +2 -0
  36. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management.rb +13 -0
  37. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_compaction.rb +23 -0
  38. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_compaction_strategy.rb +15 -0
  39. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_tool_results.rb +15 -0
  40. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_tool_results_prune_mode.rb +14 -0
  41. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice.rb +8 -0
  42. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_endpointing.rb +13 -0
  43. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_endpointing_strategy.rb +16 -0
  44. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech.rb +15 -0
  45. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_mode.rb +15 -0
  46. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_synthesizer.rb +17 -0
  47. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_synthesizer_settings.rb +13 -0
  48. data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_transcriber.rb +13 -0
  49. data/generated/lib/runtype/agents/types/post_v1agents_request_config.rb +2 -0
  50. data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management.rb +13 -0
  51. data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_compaction.rb +23 -0
  52. data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_compaction_strategy.rb +15 -0
  53. data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_tool_results.rb +15 -0
  54. data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_tool_results_prune_mode.rb +14 -0
  55. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice.rb +8 -0
  56. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_endpointing.rb +13 -0
  57. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_endpointing_strategy.rb +16 -0
  58. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech.rb +15 -0
  59. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_mode.rb +15 -0
  60. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_synthesizer.rb +17 -0
  61. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_synthesizer_settings.rb +13 -0
  62. data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_transcriber.rb +13 -0
  63. data/generated/lib/runtype/agents/types/post_v1agents_response.rb +2 -0
  64. data/generated/lib/runtype/agents/types/post_v1agents_response_sandbox_capability_issues_item.rb +19 -0
  65. data/generated/lib/runtype/agents/types/post_v1agents_response_sandbox_capability_issues_item_code.rb +13 -0
  66. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config.rb +2 -0
  67. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management.rb +13 -0
  68. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_compaction.rb +23 -0
  69. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_compaction_strategy.rb +15 -0
  70. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_tool_results.rb +15 -0
  71. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_tool_results_prune_mode.rb +14 -0
  72. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice.rb +8 -0
  73. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_endpointing.rb +13 -0
  74. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_endpointing_strategy.rb +16 -0
  75. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech.rb +15 -0
  76. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_mode.rb +15 -0
  77. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_synthesizer.rb +17 -0
  78. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_synthesizer_settings.rb +13 -0
  79. data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_transcriber.rb +13 -0
  80. data/generated/lib/runtype/agents/types/put_v1agents_id_response.rb +2 -0
  81. data/generated/lib/runtype/agents/types/put_v1agents_id_response_sandbox_capability_issues_item.rb +19 -0
  82. data/generated/lib/runtype/agents/types/put_v1agents_id_response_sandbox_capability_issues_item_code.rb +13 -0
  83. data/generated/lib/runtype/analytics/types/get_v1analytics_record_results_response_data_data_results_item.rb +4 -0
  84. data/generated/lib/runtype/client.rb +5 -0
  85. data/generated/lib/runtype/dispatch/client.rb +11 -4
  86. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent.rb +6 -0
  87. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_durability.rb +15 -0
  88. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_durability_forced.rb +14 -0
  89. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice.rb +8 -0
  90. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_endpointing.rb +13 -0
  91. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_endpointing_strategy.rb +16 -0
  92. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech.rb +15 -0
  93. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_mode.rb +15 -0
  94. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_synthesizer.rb +17 -0
  95. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_synthesizer_settings.rb +13 -0
  96. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_transcriber.rb +13 -0
  97. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent.rb +6 -0
  98. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_durability.rb +15 -0
  99. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_durability_forced.rb +14 -0
  100. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice.rb +8 -0
  101. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_endpointing.rb +13 -0
  102. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_endpointing_strategy.rb +16 -0
  103. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech.rb +15 -0
  104. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_mode.rb +15 -0
  105. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_synthesizer.rb +17 -0
  106. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_synthesizer_settings.rb +13 -0
  107. data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_transcriber.rb +13 -0
  108. data/generated/lib/runtype/evals/types/get_v1eval_id_results_response.rb +2 -0
  109. data/generated/lib/runtype/evals/types/get_v1eval_id_results_response_case_errors_item.rb +15 -0
  110. data/generated/lib/runtype/executions/client.rb +53 -0
  111. data/generated/lib/runtype/executions/types/get_v1executions_execution_id_events_request.rb +15 -0
  112. data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response.rb +6 -0
  113. data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response_agent_target_resolution.rb +15 -0
  114. data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response_agent_version.rb +16 -0
  115. data/generated/lib/runtype/flows/client.rb +2 -1
  116. data/generated/lib/runtype/flows/types/post_v1flows_ensure_request.rb +2 -0
  117. data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model.rb +2 -0
  118. data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_reasoning.rb +13 -0
  119. data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model.rb +2 -0
  120. data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_reasoning.rb +13 -0
  121. data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item.rb +2 -0
  122. data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item_reasoning.rb +13 -0
  123. data/generated/lib/runtype/records/types/get_v1records_id_results_response_data_item.rb +4 -0
  124. data/generated/lib/runtype/tools/client.rb +11 -11
  125. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request.rb +29 -0
  126. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_language.rb +15 -0
  127. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_retention.rb +16 -0
  128. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy.rb +17 -0
  129. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_idle.rb +11 -0
  130. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_idle_after.rb +16 -0
  131. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_never.rb +9 -0
  132. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response.rb +2 -0
  133. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy.rb +23 -0
  134. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_metering.rb +14 -0
  135. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_origin.rb +14 -0
  136. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_retention.rb +16 -0
  137. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy.rb +17 -0
  138. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_idle.rb +11 -0
  139. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_idle_after.rb +16 -0
  140. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_never.rb +9 -0
  141. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request.rb +35 -0
  142. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_language.rb +15 -0
  143. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_retention.rb +16 -0
  144. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy.rb +17 -0
  145. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_idle.rb +11 -0
  146. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_idle_after.rb +16 -0
  147. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_never.rb +9 -0
  148. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response.rb +2 -0
  149. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy.rb +23 -0
  150. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_metering.rb +14 -0
  151. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_origin.rb +14 -0
  152. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_retention.rb +16 -0
  153. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy.rb +17 -0
  154. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_idle.rb +11 -0
  155. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_idle_after.rb +16 -0
  156. data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_never.rb +9 -0
  157. data/generated/lib/runtype/types/agent_ensure_bad_request.rb +11 -0
  158. data/generated/lib/runtype/types/agent_ensure_bad_request_code.rb +11 -0
  159. data/generated/lib/runtype/types/agent_ensure_deploy_target.rb +15 -0
  160. data/generated/lib/runtype/types/agent_ensure_deployment.rb +20 -0
  161. data/generated/lib/runtype/types/agent_ensure_response_changed_keys.rb +2 -0
  162. data/generated/lib/runtype/types/agent_ensure_response_changed_keys_deployment.rb +20 -0
  163. data/generated/lib/runtype/types/agent_ensure_response_version_id.rb +4 -0
  164. data/generated/lib/runtype/types/agent_ensure_server_error.rb +13 -0
  165. data/generated/lib/runtype/types/agent_ensure_server_error_code.rb +11 -0
  166. data/generated/lib/runtype/types/agent_pull_response_definition_config.rb +2 -0
  167. data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management.rb +11 -0
  168. data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_compaction.rb +21 -0
  169. data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_compaction_strategy.rb +13 -0
  170. data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_tool_results.rb +13 -0
  171. data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_tool_results_prune_mode.rb +12 -0
  172. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice.rb +8 -0
  173. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_endpointing.rb +11 -0
  174. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_endpointing_strategy.rb +14 -0
  175. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech.rb +13 -0
  176. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_mode.rb +13 -0
  177. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_synthesizer.rb +15 -0
  178. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_synthesizer_settings.rb +11 -0
  179. data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_transcriber.rb +11 -0
  180. data/generated/lib/runtype/types/dispatch_continuation_json_response.rb +2 -0
  181. data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response.rb +13 -0
  182. data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response_pause_kind.rb +12 -0
  183. data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response_status.rb +11 -0
  184. data/generated/lib/runtype/types/dispatch_resume_json_response.rb +13 -0
  185. data/generated/lib/runtype/types/ensure_version_metadata.rb +13 -0
  186. data/generated/lib/runtype/types/eval_run_scores_response_cases_item.rb +2 -0
  187. data/generated/lib/runtype/types/execution_stream_event.rb +2 -0
  188. data/generated/lib/runtype/types/execution_stream_event_context_notice.rb +29 -0
  189. data/generated/lib/runtype/types/execution_stream_event_context_notice_kind.rb +11 -0
  190. data/generated/lib/runtype/types/execution_stream_event_context_notice_outcome.rb +14 -0
  191. data/generated/lib/runtype/types/execution_stream_event_context_notice_phase.rb +12 -0
  192. data/generated/lib/runtype/types/execution_stream_event_context_notice_reason.rb +15 -0
  193. data/generated/lib/runtype/types/execution_stream_event_context_notice_strategy.rb +12 -0
  194. data/generated/lib/runtype/types/not_found_error_body_code.rb +11 -0
  195. data/generated/lib/runtype/types/precondition_failed_error_body.rb +17 -0
  196. data/generated/lib/runtype/types/precondition_failed_error_body_code.rb +11 -0
  197. data/generated/lib/runtype/types/run_eval_response_cases_item.rb +2 -0
  198. data/generated/lib/runtype.rb +157 -5
  199. data/lib/runtype/agent_definition.rb +331 -0
  200. data/lib/runtype/agent_ensure.rb +110 -0
  201. data/lib/runtype/canonical_json.rb +89 -0
  202. data/lib/runtype/client.rb +86 -30
  203. data/lib/runtype/detached_follow.rb +136 -0
  204. data/lib/runtype/errors.rb +91 -1
  205. data/lib/runtype/local_tool_runner.rb +222 -0
  206. data/lib/runtype/local_tools.rb +258 -0
  207. data/lib/runtype/sdk_version.rb +1 -1
  208. data/lib/runtype.rb +7 -1
  209. metadata +160 -2
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "uri"
4
5
 
5
6
  module Runtype
6
7
  # The Runtype API client.
@@ -24,6 +25,9 @@ module Runtype
24
25
  # Configuration is per client. There is no global state to set up, so two
25
26
  # clients with different keys or base URLs can coexist in one process.
26
27
  class Client
28
+ include Runtype::AgentEnsure
29
+ include Runtype::LocalToolRunner
30
+
27
31
  # Per-operation socket timeout, in seconds. Generous by default: an
28
32
  # execution stream can idle between events while a model is thinking.
29
33
  DEFAULT_TIMEOUT = 600
@@ -109,6 +113,26 @@ module Runtype
109
113
  stream("v1/dispatch", dispatch_body(params, true), request_options, &block)
110
114
  end
111
115
 
116
+ # Follows an execution through the execution-scoped events alias
117
+ # (`GET /v1/executions/{executionId}/events`) and streams what it emits.
118
+ # This is the reattach handle for a run that keeps going on the platform
119
+ # after its request returned: a durable-lane resume answers
120
+ # `{ executionId, status: "running" }` and expects the caller to read the
121
+ # rest of the turn here.
122
+ #
123
+ # @param execution_id [String]
124
+ # @param after [String, nil] the last event `seq` or SSE id already
125
+ # consumed, so the replay starts strictly past it.
126
+ # @param conversation_id [String, nil]
127
+ # @param request_options [Hash] see {#dispatch}.
128
+ # @yieldparam event [Runtype::Streaming::Event]
129
+ # @return [Runtype::Streaming::EventStream]
130
+ # @raise [Runtype::APIStatusError] on a non-2xx response.
131
+ def execution_events_stream(execution_id, after: nil, conversation_id: nil, request_options: {}, &block)
132
+ query = { after: after, conversationId: conversation_id }.compact
133
+ stream("v1/executions/#{execution_id}/events", nil, request_options, method: "GET", query: query, &block)
134
+ end
135
+
112
136
  # Resumes a paused execution with the outputs of its client-executed tools.
113
137
  # Does not count against execution limits.
114
138
  #
@@ -188,20 +212,35 @@ module Runtype
188
212
  end
189
213
 
190
214
  # Forces the streaming preference the caller asked for, leaving the rest of
191
- # the request body untouched. Accepts an `options` hash keyed either way.
215
+ # the request body untouched.
192
216
  #
193
217
  # @param params [Hash]
194
218
  # @param streaming [Boolean]
195
219
  # @return [Hash]
196
220
  def with_stream_option(params, streaming)
197
- body = params.dup
198
- key = body.key?(:options) ? :options : "options"
199
- options = (body[key] || {}).dup
200
- options.delete(:streamResponse)
201
- options.delete("streamResponse")
202
- options[:streamResponse] = streaming
203
- body[key] = options
204
- body
221
+ rewrite_body_key(params, :options) do |options|
222
+ options = (options || {}).dup
223
+ options.delete(:streamResponse)
224
+ options.delete("streamResponse")
225
+ options.merge(streamResponse: streaming)
226
+ end
227
+ end
228
+
229
+ # Copies `body` and replaces one top-level key with the block's result.
230
+ # The key is looked up as a Symbol first, then as a String, and rewritten
231
+ # under whichever spelling the caller used (Symbol when absent), so every
232
+ # dispatch-body normalization agrees on one rule.
233
+ #
234
+ # @param body [Hash]
235
+ # @param key [Symbol]
236
+ # @yieldparam current [Object, nil] the existing value.
237
+ # @return [Hash]
238
+ def rewrite_body_key(body, key)
239
+ found = key
240
+ found = key.to_s if !body.key?(key) && body.key?(key.to_s)
241
+ copy = body.dup
242
+ copy[found] = yield(copy[found])
243
+ copy
205
244
  end
206
245
 
207
246
  # Builds the dispatch request body: validates the target, rejects
@@ -224,7 +263,8 @@ module Runtype
224
263
  params.key?(:agent) || params.key?("agent")
225
264
 
226
265
  raise ArgumentError,
227
- "Dispatch requires either a flow or an agent target; pass flow: <id> or agent: <id>."
266
+ "Dispatch requires either a flow or an agent target; " \
267
+ "pass flow: { id: \"flow_...\" } or agent: { agentId: \"agent_...\" }."
228
268
  end
229
269
 
230
270
  # @param params [Hash]
@@ -253,24 +293,22 @@ module Runtype
253
293
  end
254
294
  end
255
295
 
256
- # Copies the body so the caller's nested `record` is not mutated, then
257
- # coerces `record.id` to a string to match the canonical wire contract.
296
+ # Coerces `record.id` to a String to match the canonical wire contract,
297
+ # without mutating the caller's nested `record`.
258
298
  #
259
299
  # @param body [Hash]
260
300
  # @return [Hash]
261
301
  def normalize_record_id!(body)
262
- key = body.key?(:record) ? :record : "record"
263
- record = body[key]
264
- return body unless record.is_a?(Hash)
302
+ return body unless body.key?(:record) || body.key?("record")
265
303
 
266
- id_key = record.key?(:id) ? :id : "id"
267
- return body unless record.key?(id_key)
304
+ rewrite_body_key(body, :record) do |record|
305
+ next record unless record.is_a?(Hash)
268
306
 
269
- copy = body.dup
270
- record_copy = record.dup
271
- record_copy[id_key] = String(record[id_key])
272
- copy[key] = record_copy
273
- copy
307
+ id_key = record.key?(:id) ? :id : "id"
308
+ next record unless record.key?(id_key) && !record[id_key].nil?
309
+
310
+ record.merge(id_key => record[id_key].to_s)
311
+ end
274
312
  end
275
313
 
276
314
  # @return [Hash]
@@ -300,17 +338,31 @@ module Runtype
300
338
 
301
339
  # @return [Hash] the decoded response body.
302
340
  def post_json(path, payload, request_options)
303
- connection = open_connection(path, payload, request_options, streaming: false)
341
+ read_json(open_connection(path, payload, request_options, streaming: false))
342
+ end
343
+
344
+ # @param query [Hash] query parameters.
345
+ # @return [Hash] the decoded response body.
346
+ def get_json(path, query, request_options)
347
+ read_json(open_connection(path, nil, request_options, streaming: false, method: "GET", query: query))
348
+ end
349
+
350
+ # Reads a non-streaming response to completion, raises on a non-2xx
351
+ # status, and decodes the body.
352
+ #
353
+ # @param connection [Runtype::Streaming::Connection] a started connection.
354
+ # @return [Hash]
355
+ def read_json(connection)
304
356
  body = connection.read_body
305
357
  raise_for_status(connection, body)
306
358
  body.strip.empty? ? {} : parse_json(body)
307
359
  ensure
308
- connection&.close
360
+ connection.close
309
361
  end
310
362
 
311
363
  # @return [Runtype::Streaming::EventStream]
312
- def stream(path, payload, request_options, &block)
313
- connection = open_connection(path, payload, request_options, streaming: true)
364
+ def stream(path, payload, request_options, method: "POST", query: nil, &block)
365
+ connection = open_connection(path, payload, request_options, streaming: true, method: method, query: query)
314
366
  begin
315
367
  raise_for_status(connection, connection.read_body) unless success?(connection.status)
316
368
  rescue StandardError
@@ -325,13 +377,17 @@ module Runtype
325
377
  event_stream
326
378
  end
327
379
 
380
+ # @param query [Hash, nil] appended to the URL as a query string.
328
381
  # @return [Runtype::Streaming::Connection]
329
- def open_connection(path, payload, request_options, streaming:)
382
+ def open_connection(path, payload, request_options, streaming:, method: "POST", query: nil)
383
+ url = url_for(path, request_options)
384
+ url = "#{url}?#{URI.encode_www_form(query)}" if query && !query.empty?
330
385
  Runtype::Streaming::Connection.new(
331
- url: url_for(path, request_options),
386
+ url: url,
332
387
  headers: headers(streaming: streaming, request_options: request_options),
333
- body: JSON.generate(payload),
334
- timeout: timeout_for(request_options)
388
+ body: payload.nil? ? nil : JSON.generate(payload),
389
+ timeout: timeout_for(request_options),
390
+ method: method
335
391
  ).start
336
392
  end
337
393
 
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runtype
4
+ # Following a run that detached from its request.
5
+ #
6
+ # A saved-agent dispatch on the platform's durable lane keeps running inside
7
+ # its session after the request returns. Handing it tool outputs answers with
8
+ # an acknowledgement instead of a buffered turn:
9
+ #
10
+ # { "executionId" => "aex_…", "pauseKind" => "client_tool", "status" => "running" }
11
+ #
12
+ # and a streamed resume closes on an `await` frame whose `awaitReason` is
13
+ # `detached`. Either way the rest of the turn is read back through the
14
+ # execution-scoped events alias ({Runtype::Client#execution_events_stream}).
15
+ # This module folds that replay into the shapes the local-tool loop already
16
+ # consumes: a buffered continuation envelope (`executionId`, `events[]`,
17
+ # `status`) for the JSON loop, and a live stream for the streaming loop.
18
+ module DetachedFollow
19
+ module_function
20
+
21
+ # Local-tool awaits the events alias can carry.
22
+ AWAIT_TYPES = %w[await flow_await step_await].freeze
23
+
24
+ # The message the buffered envelope carries when the events stream closed
25
+ # before a terminal frame.
26
+ STREAM_CLOSED_EARLY =
27
+ "The execution detached and its events stream closed before a terminal frame; " \
28
+ "reattach with the execution events route."
29
+
30
+ # A buffered resume that acknowledged a detached run rather than answering
31
+ # the turn: `status: "running"`, an execution id, and no `events[]`.
32
+ #
33
+ # @param response [Object]
34
+ # @return [Boolean]
35
+ def detached_ack?(response)
36
+ return false unless response.is_a?(Hash)
37
+
38
+ id = response["executionId"]
39
+ response["status"] == "running" && id.is_a?(String) && !id.empty? && !response["events"].is_a?(Array)
40
+ end
41
+
42
+ # The highest unified `seq` among buffered events, as the `after` cursor a
43
+ # follow starts past, so the replay never re-delivers the await the outputs
44
+ # just answered.
45
+ #
46
+ # @param events [Object]
47
+ # @return [String, nil]
48
+ def resume_cursor_of(events)
49
+ return nil unless events.is_a?(Array)
50
+
51
+ seqs = events.filter_map do |event|
52
+ data = event.respond_to?(:to_h) ? event.to_h : event
53
+ seq = data["seq"] if data.is_a?(Hash)
54
+ seq if seq.is_a?(Numeric)
55
+ end
56
+ seqs.max&.to_s
57
+ end
58
+
59
+ # @param data [Hash]
60
+ # @return [Boolean]
61
+ def detached_await?(data)
62
+ data["type"] == "await" && data["awaitReason"] == Runtype::Streaming::Event::DETACHED_AWAIT_REASON
63
+ end
64
+
65
+ # @param data [Hash]
66
+ # @return [Boolean]
67
+ def local_tool_await?(data)
68
+ AWAIT_TYPES.include?(data["type"]) && !detached_await?(data)
69
+ end
70
+
71
+ # One followed leg: the buffered continuation envelope a non-streaming
72
+ # loop expects, plus the reconnect cursor the next follow starts past.
73
+ Followed = Struct.new(:envelope, :cursor, keyword_init: true)
74
+
75
+ # Reads the events alias to the run's next pause or terminal frame. The
76
+ # alias carries its own watch lease: when a leg closes on a detached
77
+ # `await`, the run is still going and the next leg reopens past the last
78
+ # event this one delivered, exactly as a dispatch stream reattaches.
79
+ #
80
+ # @param client [Runtype::Client]
81
+ # @param execution_id [String]
82
+ # @param after [String, nil]
83
+ # @param request_options [Hash]
84
+ # @return [Followed] whose envelope carries `success`, `executionId`,
85
+ # `events`, `status`, and `error` when the run failed or its stream
86
+ # closed early.
87
+ def follow_buffered(client, execution_id, after:, request_options:)
88
+ events = []
89
+ status = nil
90
+ error = nil
91
+ cursor = after
92
+
93
+ loop do
94
+ detached = false
95
+ client.execution_events_stream(execution_id, after: cursor, request_options: request_options) do |event|
96
+ data = event.data
97
+ cursor = event.id || (data["seq"].is_a?(Numeric) ? data["seq"].to_s : cursor)
98
+ detached = detached_await?(data)
99
+ next if detached
100
+
101
+ events << data
102
+ case data["type"]
103
+ when "execution_complete"
104
+ status = data["success"] == false ? "failed" : "completed"
105
+ error = data["error"] if data["error"].is_a?(String)
106
+ when "execution_error"
107
+ status = "failed"
108
+ error = error_message(data["error"])
109
+ else
110
+ status = "paused" if status.nil? && local_tool_await?(data)
111
+ end
112
+ end
113
+ break unless status.nil? && detached
114
+ end
115
+
116
+ if status.nil?
117
+ status = "failed"
118
+ error = STREAM_CLOSED_EARLY
119
+ end
120
+
121
+ envelope = { "success" => status == "completed", "executionId" => execution_id,
122
+ "events" => events, "status" => status }
123
+ envelope["error"] = error if error
124
+ Followed.new(envelope: envelope, cursor: cursor)
125
+ end
126
+
127
+ # @param detail [Object]
128
+ # @return [String]
129
+ def error_message(detail)
130
+ return detail if detail.is_a?(String)
131
+ return detail["message"].to_s if detail.is_a?(Hash) && detail.key?("message")
132
+
133
+ "Execution failed"
134
+ end
135
+ end
136
+ end
@@ -70,7 +70,6 @@ module Runtype
70
70
 
71
71
  "API error: #{status}"
72
72
  end
73
- private_class_method :message_from
74
73
  end
75
74
 
76
75
  # An error response from the API. Raised directly for status codes without a
@@ -104,6 +103,7 @@ module Runtype
104
103
  when 401 then AuthenticationError
105
104
  when 403 then PermissionDeniedError
106
105
  when 404 then NotFoundError
106
+ when 409 then ConflictError
107
107
  when 422 then UnprocessableEntityError
108
108
  when 429 then RateLimitError
109
109
  else APIStatusError
@@ -125,6 +125,96 @@ module Runtype
125
125
  # 404.
126
126
  class NotFoundError < APIStatusError; end
127
127
 
128
+ # 409.
129
+ class ConflictError < APIStatusError; end
130
+
131
+ # 409 from the agent ensure protocol: the remote changed underneath the
132
+ # caller. `code` is `"external_modification"` (the agent's last write came
133
+ # from the dashboard/API/MCP; pass `on_conflict: "overwrite"` to converge
134
+ # over it) or `"remote_changed"` (the `expected_remote_hash` no longer
135
+ # matches; re-run the dry run).
136
+ class EnsureConflictError < ConflictError
137
+ PROTOCOL_CODES = %w[external_modification remote_changed].freeze
138
+
139
+ # @return [String] `"external_modification"` or `"remote_changed"`.
140
+ def code
141
+ body_field("code")
142
+ end
143
+
144
+ # @return [String, nil] who last wrote the agent (external_modification).
145
+ def last_modified_source
146
+ body_field("lastModifiedSource")
147
+ end
148
+
149
+ # @return [String, nil] ISO-8601 (external_modification).
150
+ def modified_at
151
+ body_field("modifiedAt")
152
+ end
153
+
154
+ # @return [String, nil] the live hash (remote_changed).
155
+ def current_hash
156
+ body_field("currentHash")
157
+ end
158
+
159
+ # @param error [Runtype::APIStatusError]
160
+ # @return [Boolean]
161
+ def self.protocol_conflict?(error)
162
+ error.status_code == 409 && error.body.is_a?(Hash) && PROTOCOL_CODES.include?(error.body["code"])
163
+ end
164
+
165
+ # @param error [Runtype::APIStatusError]
166
+ # @return [Runtype::EnsureConflictError]
167
+ def self.from_status_error(error)
168
+ new(
169
+ Runtype::Error.message_from(error.body, error.status_code),
170
+ status_code: error.status_code, body: error.body, headers: error.headers
171
+ )
172
+ end
173
+
174
+ private
175
+
176
+ # @param key [String]
177
+ # @return [Object, nil]
178
+ def body_field(key)
179
+ body.is_a?(Hash) ? body[key] : nil
180
+ end
181
+ end
182
+
183
+ # Raised by `ensure_agent(definition, expect_no_changes: true)` when the
184
+ # platform's definition no longer matches the repo's.
185
+ class DriftError < Error
186
+ # @return [Hash] the dry-run plan: `"changes"`, `"changedKeys"`,
187
+ # `"contentHash"`, `"remoteHash"`, `"agentId"`.
188
+ attr_reader :plan
189
+
190
+ # @param plan [Hash]
191
+ # @param name [String, nil]
192
+ def initialize(plan, name: nil)
193
+ @plan = plan
194
+ changed = Array(plan["changedKeys"]).join(", ")
195
+ super(
196
+ "Agent #{(name || plan["agentId"] || "definition").inspect} drifted: plan is #{plan["changes"].inspect} " \
197
+ "(changed: #{changed.empty? ? "n/a" : changed}). Run client.pull_agent(name) to absorb the remote edit " \
198
+ "into your repo, or re-run ensure_agent to converge."
199
+ )
200
+ end
201
+ end
202
+
203
+ # A local tool could not be run during `run_with_local_tools`: the server
204
+ # paused on a tool with no handler, the handler raised (the original error is
205
+ # `#cause`), or the loop exceeded its round limit.
206
+ class LocalToolError < Error
207
+ # @return [String, nil] the tool the server paused on.
208
+ attr_reader :tool_name
209
+
210
+ # @param message [String]
211
+ # @param tool_name [String, nil]
212
+ def initialize(message, tool_name: nil)
213
+ @tool_name = tool_name
214
+ super(message)
215
+ end
216
+ end
217
+
128
218
  # 422.
129
219
  class UnprocessableEntityError < APIStatusError; end
130
220
 
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runtype
4
+ # The local-tool pause/resume loop on {Runtype::Client}.
5
+ module LocalToolRunner
6
+ # Upper bound on resume legs for one call, so a tool the server keeps
7
+ # re-requesting cannot spin forever.
8
+ DEFAULT_MAX_LOCAL_TOOL_ROUNDS = 50
9
+
10
+ # Dispatches, and whenever the execution pauses on a tool in `tools`, runs
11
+ # it here and resumes with the output — repeating until the execution ends
12
+ # or pauses on something this process does not own (an approval or an
13
+ # elicitation, for instance), which is returned to the caller as-is.
14
+ #
15
+ # Every parallel local tool call in one model turn is executed
16
+ # concurrently and answered in a single resume. Outputs are keyed by the
17
+ # server's per-call id when it sent one, else by tool name.
18
+ #
19
+ # Without a block the loop runs over JSON responses and returns the final
20
+ # response body. With a block it streams: every event of every leg is
21
+ # yielded, and the last leg's closed {Runtype::Streaming::EventStream} is
22
+ # returned so its reconnect handle stays readable.
23
+ #
24
+ # result = client.run_with_local_tools(
25
+ # { "save_goals" => save_goals, "lookup_order" => ->(args) { Orders.find(args["id"]) } },
26
+ # agent: { agentId: "agent_01h..." },
27
+ # messages: [{ role: "user", content: "I want to sleep better" }]
28
+ # )
29
+ #
30
+ # @param tools [Hash{String, Symbol => Runtype::LocalTool, #call}] tool
31
+ # name => handler. A {Runtype::LocalTool} carries the schema the model
32
+ # sees; a bare callable relies on the saved agent already declaring the
33
+ # tool as a `toolType: "local"` runtime tool.
34
+ # @param scope [Symbol, String] `:session` (default) sends no tool
35
+ # declarations — the saved agent owns them. `:turn` derives
36
+ # `clientTools[]` from every {Runtype::LocalTool} in `tools` and ships
37
+ # them in the dispatch envelope for this one turn (on top of any
38
+ # `clientTools:` already in `params`).
39
+ # @param max_rounds [Integer] see {DEFAULT_MAX_LOCAL_TOOL_ROUNDS}.
40
+ # @param request_options [Hash] see {Runtype::Client#dispatch}.
41
+ # @param params [Hash] the dispatch request body (`agent:`, `messages:`, ...).
42
+ # @yieldparam event [Runtype::Streaming::Event] when streaming.
43
+ # @return [Hash, Runtype::Streaming::EventStream]
44
+ # @raise [Runtype::LocalToolError] when a paused tool has no handler, a
45
+ # handler raises, or `max_rounds` is exceeded.
46
+ def run_with_local_tools(tools, scope: :session, max_rounds: DEFAULT_MAX_LOCAL_TOOL_ROUNDS,
47
+ request_options: {}, **params, &block)
48
+ registry = Runtype::LocalTools::Registry.new(tools)
49
+ params = with_turn_scoped_client_tools(params, registry) if scope.to_s == "turn"
50
+
51
+ if block
52
+ stream_with_local_tools(registry, params, max_rounds, request_options, &block)
53
+ else
54
+ dispatch_with_local_tools(registry, params, max_rounds, request_options)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # @return [Hash] the final JSON response body.
61
+ def dispatch_with_local_tools(registry, params, max_rounds, request_options)
62
+ response = dispatch(request_options: request_options, **params)
63
+ answered = AnsweredCalls.new
64
+ replayed = false
65
+ cursor = nil
66
+ rounds = 0
67
+
68
+ while response["status"] == "paused"
69
+ collector = Runtype::LocalTools::PauseCollector.new
70
+ Array(response["events"]).each { |event| collector.observe(event) }
71
+ collector.observe_paused_reason(response["pausedReason"]) unless collector.paused?
72
+ return response unless collector.paused?
73
+
74
+ raise_local_tool_round_limit(max_rounds) if (rounds += 1) > max_rounds
75
+ cursor ||= Runtype::DetachedFollow.resume_cursor_of(response["events"])
76
+ batch = replayed ? answered.fresh(collector.batch) : answered.record(collector.batch)
77
+ if batch.any?
78
+ response = resume(
79
+ execution_id: collector.batch_execution_id,
80
+ tool_outputs: registry.execute(batch),
81
+ request_options: request_options
82
+ )
83
+ # A buffered continuation answers the leg in full; only a durable-lane
84
+ # acknowledgement needs the run's events followed instead.
85
+ next unless Runtype::DetachedFollow.detached_ack?(response)
86
+ end
87
+
88
+ # Followed events can replay the await these outputs just answered;
89
+ # an all-answered batch means the run is still going, so read on.
90
+ followed = Runtype::DetachedFollow.follow_buffered(
91
+ self, collector.batch_execution_id, after: cursor, request_options: request_options
92
+ )
93
+ response = followed.envelope
94
+ cursor = followed.cursor
95
+ replayed = true
96
+ end
97
+
98
+ response
99
+ end
100
+
101
+ # @return [Runtype::Streaming::EventStream] the last leg's stream.
102
+ def stream_with_local_tools(registry, params, max_rounds, request_options)
103
+ leg = StreamLeg.new
104
+ stream = dispatch_stream(request_options: request_options, **params) do |event|
105
+ leg.observe(event)
106
+ yield event
107
+ end
108
+ answered = AnsweredCalls.new
109
+ rounds = 0
110
+
111
+ loop do
112
+ batch = leg.replayed? ? answered.fresh(leg.collector.batch) : answered.record(leg.collector.batch)
113
+ if batch.any?
114
+ raise_local_tool_round_limit(max_rounds) if (rounds += 1) > max_rounds
115
+ outputs = registry.execute(batch)
116
+ execution_id = leg.collector.batch_execution_id
117
+ leg = StreamLeg.new
118
+ stream = resume_stream(
119
+ execution_id: execution_id, tool_outputs: outputs, request_options: request_options
120
+ ) do |event|
121
+ leg.observe(event)
122
+ yield event
123
+ end
124
+ elsif leg.follow?
125
+ # The leg closed on a detached `await`, or replayed only awaits this
126
+ # loop already answered: the run kept going on the platform, so its
127
+ # events are followed from where this leg stopped.
128
+ raise_local_tool_round_limit(max_rounds) if (rounds += 1) > max_rounds
129
+ execution_id = leg.execution_id || stream.execution_id
130
+ after = stream.last_event_id
131
+ leg = StreamLeg.new(replayed: true)
132
+ stream = execution_events_stream(execution_id, after: after, request_options: request_options) do |event|
133
+ leg.observe(event)
134
+ yield event
135
+ end
136
+ else
137
+ break
138
+ end
139
+ end
140
+
141
+ stream
142
+ end
143
+
144
+ # The local-tool calls one `run_with_local_tools` invocation has already
145
+ # answered. A followed replay can re-deliver an `await` whose output the
146
+ # server already holds; answering it again would run the handler twice.
147
+ class AnsweredCalls
148
+ def initialize
149
+ @keys = {}
150
+ end
151
+
152
+ # A leg the server answered directly (a dispatch or resume continuation)
153
+ # is authoritative: every call in it is new, even a repeat of a key the
154
+ # server re-requested.
155
+ #
156
+ # @param batch [Array<Runtype::LocalTools::PausedCall>]
157
+ # @return [Array<Runtype::LocalTools::PausedCall>] `batch`, recorded.
158
+ def record(batch)
159
+ batch.each { |call| @keys[call.output_key] = true }
160
+ end
161
+
162
+ # A followed replay can carry awaits already answered.
163
+ #
164
+ # @param batch [Array<Runtype::LocalTools::PausedCall>]
165
+ # @return [Array<Runtype::LocalTools::PausedCall>] the calls not yet
166
+ # answered, now recorded.
167
+ def fresh(batch)
168
+ record(batch.reject { |call| @keys.key?(call.output_key) })
169
+ end
170
+ end
171
+ private_constant :AnsweredCalls
172
+
173
+ # One streamed leg of the loop: the local-tool pauses it carried and
174
+ # whether it closed on a detached await.
175
+ class StreamLeg
176
+ attr_reader :collector, :execution_id
177
+
178
+ # @param replayed [Boolean] whether this leg reads the events alias,
179
+ # which can re-deliver awaits already answered.
180
+ def initialize(replayed: false)
181
+ @collector = Runtype::LocalTools::PauseCollector.new
182
+ @replayed = replayed
183
+ @detached = false
184
+ @execution_id = nil
185
+ end
186
+
187
+ # @return [Boolean]
188
+ def replayed?
189
+ @replayed
190
+ end
191
+
192
+ # @param event [Runtype::Streaming::Event]
193
+ # @return [void]
194
+ def observe(event)
195
+ @collector.observe(event)
196
+ @execution_id ||= event.execution_id
197
+ @detached = true if event.detached?
198
+ end
199
+
200
+ # @return [Boolean] the leg ended without a terminal frame while the run
201
+ # continues: on a detached await, or on awaits already answered.
202
+ def follow?
203
+ @detached || @collector.paused?
204
+ end
205
+ end
206
+ private_constant :StreamLeg
207
+
208
+ # @raise [Runtype::LocalToolError]
209
+ def raise_local_tool_round_limit(max_rounds)
210
+ raise Runtype::LocalToolError, "Execution kept pausing on local tools after #{max_rounds} resume rounds"
211
+ end
212
+
213
+ # @return [Hash] `params` with the registry's schema-carrying tools appended
214
+ # to `clientTools` (keyed however the caller keyed it).
215
+ def with_turn_scoped_client_tools(params, registry)
216
+ derived = registry.client_tools
217
+ return params if derived.empty?
218
+
219
+ rewrite_body_key(params, :clientTools) { |existing| Array(existing) + derived }
220
+ end
221
+ end
222
+ end