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
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Runtype
6
+ # Builds an {AgentDefinition}: the pure-local, no-I/O half of agent
7
+ # config-as-code. Converge it onto the platform with
8
+ # {Runtype::Client#ensure_agent}.
9
+ #
10
+ # assistant = Runtype.define_agent(
11
+ # name: "Pricing Assistant",
12
+ # model: "claude-sonnet-4-6",
13
+ # system_prompt: render_prompt(pricing_data),
14
+ # loop_config: { maxTurns: 1 }
15
+ # )
16
+ #
17
+ # Top-level fields accept snake_case or the wire's camelCase
18
+ # (`system_prompt:` and `systemPrompt:` are the same field). Nested config
19
+ # (`loop_config`, `tools`, ...) is passed through in wire shape untouched:
20
+ # its keys can be tool names such as `"save_patient_goals"`, so they are
21
+ # never case-converted.
22
+ #
23
+ # @param fields [Hash] `name:` plus any of the agent runtime config keys
24
+ # ({AgentDefinition::CONFIG_KEYS}), `description:` and `icon:`.
25
+ # @return [Runtype::AgentDefinition]
26
+ # @raise [ArgumentError] on a missing name, an unknown field, or an
27
+ # account-scoped `tool_…` / `agent_…` / `flow_…` reference.
28
+ def self.define_agent(**fields)
29
+ AgentDefinition.new(fields)
30
+ end
31
+
32
+ # A repo-owned agent definition, validated locally and hashed exactly as the
33
+ # API hashes it (see {#content_hash}). Immutable once built: every String,
34
+ # Hash and Array it holds is a frozen copy of the caller's input.
35
+ class AgentDefinition
36
+ # The agent runtime config surface that `ensure` converges. Keys outside
37
+ # this list are rejected by {Runtype.define_agent} and, on the wire, are
38
+ # excluded from the content hash so a stray field cannot produce a hash
39
+ # the server can never reproduce. Pinned against the generated
40
+ # `AgentPullResponseDefinitionConfig` model by the test suite.
41
+ CONFIG_KEYS = %w[
42
+ model systemPrompt temperature maxTokens topP topK frequencyPenalty
43
+ presencePenalty seed tools reasoning advisor loopConfig voice
44
+ errorHandling artifacts loggingPolicy piiRedaction temporal memory
45
+ sandbox tenancyStrategy durability state contextManagement
46
+ ].freeze
47
+
48
+ TOP_LEVEL_KEYS = (%w[name description icon] + CONFIG_KEYS).freeze
49
+
50
+ # snake_case spellings of every multi-word top-level key, derived from
51
+ # {CONFIG_KEYS} so the two can never disagree.
52
+ SNAKE_CASE_ALIASES = CONFIG_KEYS.to_h { |key| [key.gsub(/[A-Z]/) { |upper| "_#{upper.downcase}" }, key] }
53
+ .reject { |snake, camel| snake == camel }.freeze
54
+ private_constant :SNAKE_CASE_ALIASES
55
+
56
+ # Largest canonical array index (2**32 - 2). `JSON.stringify` emits such
57
+ # keys first, in ascending numeric order, ahead of every other key.
58
+ MAX_ARRAY_INDEX = 4_294_967_294
59
+ private_constant :MAX_ARRAY_INDEX
60
+
61
+ # @return [String] the agent name — its identity within the account scope.
62
+ attr_reader :name
63
+
64
+ # @return [String, nil]
65
+ attr_reader :description
66
+
67
+ # @return [String, nil]
68
+ attr_reader :icon
69
+
70
+ # @return [Hash{String => Object}] the runtime config, wire-shaped, string keys.
71
+ attr_reader :config
72
+
73
+ # @return [String] SHA-256 (hex) over {#normalized}, computed once at
74
+ # construction. Sent as the hash-only probe in the ensure protocol; the
75
+ # server recomputes and returns its own on every response.
76
+ attr_reader :content_hash
77
+
78
+ # Accepts an existing definition, the flat Hash {Runtype.define_agent}
79
+ # takes, or a wire-shaped definition with a nested `config` (what
80
+ # {Runtype::Client#pull_agent} returns), so a pulled definition can be
81
+ # ensured straight back.
82
+ #
83
+ # @param value [Runtype::AgentDefinition, Hash]
84
+ # @return [Runtype::AgentDefinition]
85
+ def self.from(value)
86
+ return value if value.is_a?(AgentDefinition)
87
+ return new(value) if value.is_a?(Hash)
88
+
89
+ raise ArgumentError, "expected a Runtype::AgentDefinition or a Hash, got #{value.class}"
90
+ end
91
+
92
+ # @param fields [Hash] the flat shape (see {Runtype.define_agent}) or the
93
+ # wire shape (`{"name", "description"?, "icon"?, "config" => {...}}`).
94
+ def initialize(fields)
95
+ raise ArgumentError, "define_agent requires a definition Hash" unless fields.is_a?(Hash)
96
+
97
+ normalized = flatten_wire_shape(fields.to_h { |key, value| [canonical_key(key), value] })
98
+ unknown = normalized.keys - TOP_LEVEL_KEYS
99
+ unless unknown.empty?
100
+ raise ArgumentError,
101
+ "define_agent: unknown field(s): #{unknown.join(", ")}. Allowed fields are name, description, icon, " \
102
+ "and the agent runtime config surface (#{CONFIG_KEYS.join(", ")})."
103
+ end
104
+
105
+ @name = normalized["name"]
106
+ unless @name.is_a?(String) && !@name.empty?
107
+ raise ArgumentError, "define_agent requires a non-empty String \"name\""
108
+ end
109
+
110
+ @name = @name.dup.freeze
111
+ @description = normalized["description"]&.dup&.freeze
112
+ @icon = normalized["icon"]&.dup&.freeze
113
+ @config = CONFIG_KEYS.each_with_object({}) do |key, config|
114
+ value = normalized[key]
115
+ config[key] = deep_freeze(deep_stringify_keys(value)) unless value.nil?
116
+ end.freeze
117
+
118
+ non_portable = non_portable_refs(@config)
119
+ unless non_portable.empty?
120
+ raise ArgumentError,
121
+ "define_agent: account-scoped reference(s) at #{non_portable.join(", ")}. Definitions must be " \
122
+ "environment-portable — tool_…/agent_…/flow_… IDs belong to one account/environment. Use " \
123
+ "builtin:/platform:/mcp: references, or reference a saved resource by name — tool:<name>, " \
124
+ "agent:<name>, or flow:<name> instead."
125
+ end
126
+
127
+ @content_hash = self.class.content_hash_for(to_h)
128
+ freeze
129
+ end
130
+
131
+ # The wire definition sent to `POST /v1/agents/ensure`.
132
+ #
133
+ # @return [Hash{String => Object}]
134
+ def to_h
135
+ wire = { "name" => name }
136
+ wire["description"] = description unless description.nil?
137
+ wire["icon"] = icon unless icon.nil?
138
+ wire["config"] = config
139
+ wire
140
+ end
141
+
142
+ # The canonical normalized form the content hash is computed over: keys
143
+ # ordered as `JSON.stringify` emits them, nil entries dropped, empty
144
+ # description/icon dropped, config restricted to {CONFIG_KEYS} (unknown
145
+ # keys excluded). Byte-identical to the TypeScript SDK's
146
+ # `normalizeAgentDefinition` once serialized.
147
+ #
148
+ # @param definition [Hash] a wire-shaped definition
149
+ # (`{"name", "description"?, "icon"?, "config"?}`), such as the one
150
+ # {Runtype::Client#pull_agent} returns.
151
+ # @return [Hash{String => Object}]
152
+ def self.normalize(definition)
153
+ wire = definition.to_h { |key, value| [key.to_s, value] }
154
+ description = wire["description"]
155
+ icon = wire["icon"]
156
+ raw_config = wire["config"].is_a?(Hash) ? wire["config"].to_h { |key, value| [key.to_s, value] } : {}
157
+
158
+ canonical = { "name" => wire["name"] }
159
+ canonical["description"] = description if description.is_a?(String) && !description.empty?
160
+ canonical["icon"] = icon if icon.is_a?(String) && !icon.empty?
161
+ canonical["config"] = CONFIG_KEYS.sort.each_with_object({}) do |key, out|
162
+ value = raw_config[key]
163
+ out[key] = normalize_value(value) unless value.nil?
164
+ end
165
+ canonical
166
+ end
167
+
168
+ # SHA-256 (hex) over the canonical normalized form of a wire-shaped
169
+ # definition. Matches the hash the API returns for the same definition.
170
+ #
171
+ # @param definition [Hash] see {.normalize}.
172
+ # @return [String]
173
+ def self.content_hash_for(definition)
174
+ Digest::SHA256.hexdigest(Runtype::CanonicalJSON.generate(normalize(definition)))
175
+ end
176
+
177
+ # Drops nil entries and orders object keys the way V8's `JSON.stringify`
178
+ # emits them — canonical array indexes first in ascending numeric order,
179
+ # then the remaining keys sorted by UTF-16 code units — recursively; arrays
180
+ # keep their order. The TypeScript normalizer sorts keys and then
181
+ # serializes, and serialization applies this order, so hashing the same
182
+ # bytes requires it here.
183
+ #
184
+ # @param value [Object]
185
+ # @return [Object]
186
+ def self.normalize_value(value)
187
+ case value
188
+ when Array
189
+ value.map { |entry| normalize_value(entry) }
190
+ when Hash
191
+ entries = value.to_h { |key, entry| [key.to_s, entry] }
192
+ entries.keys.sort_by { |key| json_key_order(key) }.each_with_object({}) do |key, out|
193
+ out[key] = normalize_value(entries[key]) unless entries[key].nil?
194
+ end
195
+ else
196
+ value
197
+ end
198
+ end
199
+ private_class_method :normalize_value
200
+
201
+ # @param key [String]
202
+ # @return [Array] a sort key: `[0, index]` for canonical array indexes,
203
+ # `[1, utf16 code units]` for everything else.
204
+ def self.json_key_order(key)
205
+ if key.match?(/\A(?:0|[1-9]\d*)\z/) && key.to_i <= MAX_ARRAY_INDEX
206
+ [0, key.to_i, []]
207
+ else
208
+ [1, 0, key.encode("UTF-16LE").unpack("v*")]
209
+ end
210
+ end
211
+ private_class_method :json_key_order
212
+
213
+ # @return [Hash{String => Object}] see {.normalize}.
214
+ def normalized
215
+ self.class.normalize(to_h)
216
+ end
217
+
218
+ # @return [Boolean]
219
+ def ==(other)
220
+ other.is_a?(AgentDefinition) && other.name == name && other.description == description &&
221
+ other.icon == icon && other.config == config
222
+ end
223
+ alias eql? ==
224
+
225
+ # @return [Integer]
226
+ def hash
227
+ content_hash.hash
228
+ end
229
+
230
+ # @return [String]
231
+ def inspect
232
+ "#<#{self.class.name} name=#{name.inspect} model=#{config["model"].inspect}>"
233
+ end
234
+
235
+ private
236
+
237
+ # @param key [String, Symbol]
238
+ # @return [String]
239
+ def canonical_key(key)
240
+ string = key.to_s
241
+ SNAKE_CASE_ALIASES.fetch(string, string)
242
+ end
243
+
244
+ # Lifts a wire-shaped `config` Hash into the flat authoring shape, so both
245
+ # spellings validate through the same path.
246
+ #
247
+ # @param fields [Hash{String => Object}]
248
+ # @return [Hash{String => Object}]
249
+ def flatten_wire_shape(fields)
250
+ return fields unless fields["config"].is_a?(Hash)
251
+
252
+ config = fields["config"].to_h { |key, value| [canonical_key(key), value] }
253
+ fields.except("config").merge(config)
254
+ end
255
+
256
+ # @param value [Object]
257
+ # @return [Object] a fresh copy of the structure with String keys throughout.
258
+ def deep_stringify_keys(value)
259
+ case value
260
+ when Hash then value.to_h { |key, entry| [key.to_s, deep_stringify_keys(entry)] }
261
+ when Array then value.map { |entry| deep_stringify_keys(entry) }
262
+ when String then value.dup
263
+ else value
264
+ end
265
+ end
266
+
267
+ # Freezes every Hash, Array and String in a structure the constructor just
268
+ # copied, so the definition that was validated is the one that gets hashed
269
+ # and sent: no caller can reach through {#config} or {#to_h} and slip an
270
+ # account-scoped reference past the constructor checks.
271
+ #
272
+ # @param value [Object]
273
+ # @return [Object] the same value, frozen throughout.
274
+ def deep_freeze(value)
275
+ case value
276
+ when Hash then value.each_value { |entry| deep_freeze(entry) }
277
+ when Array then value.each { |entry| deep_freeze(entry) }
278
+ end
279
+ value.freeze
280
+ end
281
+
282
+ # Collects account-scoped references, which the v1 ensure surface rejects
283
+ # because a definition must converge every environment unchanged.
284
+ # Mirrors `collectNonPortableToolRefs` in the TypeScript SDK.
285
+ #
286
+ # @param config [Hash]
287
+ # @return [Array<String>] JSON-path-ish locations of the offending refs.
288
+ def non_portable_refs(config)
289
+ tools = config["tools"]
290
+ return [] unless tools.is_a?(Hash)
291
+
292
+ found = []
293
+ scan_array = lambda do |value, path|
294
+ next unless value.is_a?(Array)
295
+
296
+ value.each_with_index { |ref, index| found << "#{path}[#{index}]" if account_scoped?(ref) }
297
+ end
298
+ scan_keys = lambda do |value, path|
299
+ next unless value.is_a?(Hash)
300
+
301
+ value.each_key { |key| found << "#{path}.#{key}" if account_scoped?(key) }
302
+ end
303
+
304
+ scan_array.call(tools["toolIds"], "tools.toolIds")
305
+ scan_keys.call(tools["toolConfigs"], "tools.toolConfigs")
306
+ scan_keys.call(tools["perToolLimits"], "tools.perToolLimits")
307
+ scan_array.call(tools.dig("approval", "require"), "tools.approval.require")
308
+ scan_array.call(tools.dig("subagentConfig", "toolPool"), "tools.subagentConfig.toolPool")
309
+ scan_array.call(tools.dig("codeModeConfig", "toolPool"), "tools.codeModeConfig.toolPool")
310
+
311
+ Array(tools["runtimeTools"]).each_with_index do |runtime_tool, index|
312
+ next unless runtime_tool.is_a?(Hash) && runtime_tool["config"].is_a?(Hash)
313
+
314
+ base = "tools.runtimeTools[#{index}].config"
315
+ tool_config = runtime_tool["config"]
316
+ if runtime_tool["toolType"] == "subagent" && tool_config["agentId"].to_s.start_with?("agent_")
317
+ found << "#{base}.agentId"
318
+ elsif runtime_tool["toolType"] == "flow" && tool_config["flowId"].to_s.start_with?("flow_")
319
+ found << "#{base}.flowId"
320
+ end
321
+ end
322
+ found
323
+ end
324
+
325
+ # @param ref [Object]
326
+ # @return [Boolean]
327
+ def account_scoped?(ref)
328
+ ref.is_a?(String) && ref.start_with?("tool_")
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Runtype
6
+ # Agent config-as-code on {Runtype::Client}: converge a repo-owned
7
+ # {AgentDefinition} onto the platform (`POST /v1/agents/ensure`) and pull the
8
+ # platform's definition back (`GET /v1/agents/pull`). Protocol:
9
+ # `docs/adr/0003-agent-config-as-code-ensure.md`.
10
+ module AgentEnsure
11
+ # Idempotently makes the platform's definition of this agent match
12
+ # `definition`. Identity is the agent name within the API key's account
13
+ # scope. Hash-first: probes with the local content hash and only ships the
14
+ # full definition when the server reports a miss. Every change appends an
15
+ # immutable version; nothing is ever deleted. Non-executing.
16
+ #
17
+ # @param definition [Runtype::AgentDefinition, Hash] see {Runtype.define_agent}.
18
+ # @param dry_run [Boolean] plan without writing (the CI drift gate).
19
+ # @param expect_no_changes [Boolean] implies `dry_run`; raises
20
+ # {Runtype::DriftError} unless the plan is `"none"`.
21
+ # @param on_conflict [String, Symbol, nil] `"error"` (default: 409 when the
22
+ # agent's last write came from the dashboard/API/MCP) or `"overwrite"`.
23
+ # @param release [String, Symbol, nil] `"publish"` also re-aims the
24
+ # published-version pointer; default `"none"`.
25
+ # @param expected_remote_hash [String, nil] binds a dry run to its apply:
26
+ # the write proceeds only if the remote still hashes to this value.
27
+ # @param request_options [Hash] see {Runtype::Client#dispatch}.
28
+ # @return [Hash] `{"result" => "unchanged"|"created"|"updated", "agentId",
29
+ # "versionId", "contentHash"}`, or for a dry run `{"result" => "plan",
30
+ # "changes" => "none"|"create"|"update", "changedKeys", "contentHash", ...}`.
31
+ # `contentHash` is always the server-computed canonical hash.
32
+ # @raise [Runtype::EnsureConflictError] on a 409 (`external_modification`
33
+ # or `remote_changed`).
34
+ # @raise [Runtype::DriftError] with `expect_no_changes:` when the plan is
35
+ # not `"none"`.
36
+ def ensure_agent(definition, dry_run: false, expect_no_changes: false, on_conflict: nil, release: nil,
37
+ expected_remote_hash: nil, request_options: {})
38
+ definition = Runtype::AgentDefinition.from(definition)
39
+ passthrough = {
40
+ onConflict: on_conflict&.to_s,
41
+ release: release&.to_s,
42
+ expectedRemoteHash: expected_remote_hash
43
+ }.compact
44
+
45
+ if dry_run || expect_no_changes
46
+ plan = ensure_request(
47
+ { name: definition.name, definition: definition.to_h, dryRun: true, **passthrough }, request_options
48
+ )
49
+ unless plan["result"] == "plan"
50
+ raise Runtype::Error, "Expected a plan result from dryRun, got #{plan["result"].inspect}"
51
+ end
52
+ raise Runtype::DriftError.new(plan, name: definition.name) if expect_no_changes && plan["changes"] != "none"
53
+
54
+ return plan
55
+ end
56
+
57
+ local_hash = definition.content_hash
58
+ memo_key = "#{definition.name}:#{local_hash}"
59
+ content_hash = ensure_memo[memo_key] || local_hash
60
+
61
+ probe = ensure_request({ name: definition.name, contentHash: content_hash, **passthrough }, request_options)
62
+ return memoize_ensure(memo_key, probe) unless probe["result"] == "definitionRequired"
63
+
64
+ converged = ensure_request({ name: definition.name, definition: definition.to_h, **passthrough }, request_options)
65
+ if converged["result"] == "definitionRequired"
66
+ raise Runtype::Error, "Server reported definitionRequired for a full-definition request"
67
+ end
68
+
69
+ memoize_ensure(memo_key, converged)
70
+ end
71
+
72
+ # The canonical definition and provenance of an agent by name — the
73
+ # absorb-drift direction. `contentHash` reflects the live agent state.
74
+ #
75
+ # @param name [String]
76
+ # @param request_options [Hash]
77
+ # @return [Hash] `{"agentId", "definition" => {...}, "contentHash",
78
+ # "lastModifiedSource", "updatedAt", "versionId", "warnings"?}`.
79
+ # @raise [Runtype::NotFoundError] when no agent by that name is in scope.
80
+ def pull_agent(name, request_options: {})
81
+ get_json("v1/agents/pull", { name: name }, request_options)
82
+ end
83
+
84
+ private
85
+
86
+ # @return [Hash{String => String}] `name:localHash` => server hash, so a
87
+ # hot process pays the probe once per definition.
88
+ def ensure_memo
89
+ @ensure_memo ||= {}
90
+ end
91
+
92
+ # @param memo_key [String]
93
+ # @param result [Hash]
94
+ # @return [Hash] the result, for chaining.
95
+ def memoize_ensure(memo_key, result)
96
+ ensure_memo[memo_key] = result["contentHash"] if result["result"] != "plan" && result["contentHash"]
97
+ result
98
+ end
99
+
100
+ # @return [Hash] the decoded response body.
101
+ # @raise [Runtype::EnsureConflictError] for the protocol's two 409 shapes.
102
+ def ensure_request(body, request_options)
103
+ post_json("v1/agents/ensure", body, request_options)
104
+ rescue Runtype::APIStatusError => e
105
+ raise Runtype::EnsureConflictError.from_status_error(e) if Runtype::EnsureConflictError.protocol_conflict?(e)
106
+
107
+ raise
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Runtype
6
+ # Serializes a value exactly as JavaScript's `JSON.stringify` would, so a
7
+ # SHA-256 over the result matches the hash the TypeScript SDK and the API
8
+ # compute for the same definition (the content hash is a wire contract; see
9
+ # `docs/adr/0003-agent-config-as-code-ensure.md`).
10
+ #
11
+ # Ruby's `JSON.generate` already agrees with `JSON.stringify` on strings,
12
+ # booleans, nil, arrays and objects. Numbers are the exception: Ruby prints
13
+ # `1.0` where JavaScript prints `1`, and switches to exponent notation at
14
+ # different magnitudes (`1.0e-05` vs `0.00001`). This module renders numbers
15
+ # with the ECMA-262 `Number::toString` rules over Ruby's shortest round-trip
16
+ # digits, which are the same digits V8 picks.
17
+ module CanonicalJSON
18
+ module_function
19
+
20
+ # @param value [Object] a JSON-compatible value (Hash, Array, String,
21
+ # Numeric, true, false, nil).
22
+ # @return [String] the canonical serialization.
23
+ def generate(value)
24
+ case value
25
+ when Hash
26
+ "{#{value.map { |key, entry| "#{JSON.generate(key.to_s)}:#{generate(entry)}" }.join(",")}}"
27
+ when Array
28
+ "[#{value.map { |entry| generate(entry) }.join(",")}]"
29
+ when Integer
30
+ value.to_s
31
+ when Float
32
+ number_literal(value)
33
+ when String, true, false, nil
34
+ JSON.generate(value)
35
+ else
36
+ raise ArgumentError, "CanonicalJSON cannot serialize #{value.class}"
37
+ end
38
+ end
39
+
40
+ # Renders a Float the way `Number.prototype.toString` does.
41
+ #
42
+ # @param value [Float]
43
+ # @return [String]
44
+ def number_literal(value)
45
+ raise ArgumentError, "CanonicalJSON cannot serialize #{value}" unless value.finite?
46
+ return "0" if value.zero?
47
+
48
+ sign = value.negative? ? "-" : ""
49
+ digits, exponent = shortest_digits(value.abs)
50
+ k = digits.length
51
+ n = exponent # the decimal point sits after the first n digits
52
+
53
+ body =
54
+ if n.between?(k, 21)
55
+ digits + ("0" * (n - k))
56
+ elsif n.between?(1, 21)
57
+ "#{digits[0, n]}.#{digits[n..]}"
58
+ elsif n.between?(-5, 0)
59
+ "0.#{"0" * -n}#{digits}"
60
+ else
61
+ mantissa = k == 1 ? digits : "#{digits[0]}.#{digits[1..]}"
62
+ e = n - 1
63
+ "#{mantissa}e#{e.negative? ? "-" : "+"}#{e.abs}"
64
+ end
65
+ sign + body
66
+ end
67
+
68
+ # Splits a positive finite Float into its shortest round-trip decimal
69
+ # digits (no leading or trailing zeros) and the position of the decimal
70
+ # point relative to those digits, per ECMA-262's (s, k, n) decomposition.
71
+ #
72
+ # @param value [Float] positive, finite, non-zero.
73
+ # @return [Array(String, Integer)]
74
+ def shortest_digits(value)
75
+ mantissa, exponent = value.to_s.split("e")
76
+ exp = exponent ? Integer(exponent, 10) : 0
77
+ integer_part, fraction_part = mantissa.split(".")
78
+ fraction_part ||= ""
79
+ raw = integer_part + fraction_part
80
+ point = integer_part.length + exp
81
+
82
+ stripped = raw.sub(/\A0+/, "")
83
+ point -= raw.length - stripped.length
84
+ stripped = stripped.sub(/0+\z/, "")
85
+ [stripped, point]
86
+ end
87
+ private_class_method :shortest_digits
88
+ end
89
+ end