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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +92 -6
- data/generated/lib/runtype/agent_aliases/client.rb +320 -0
- data/generated/lib/runtype/agent_aliases/types/delete_v1agents_id_aliases_alias_request.rb +17 -0
- data/generated/lib/runtype/agent_aliases/types/delete_v1agents_id_aliases_alias_response.rb +17 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_alias_request.rb +15 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_alias_response.rb +29 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_request.rb +17 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_response.rb +13 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_aliases_response_data_item.rb +29 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_request.rb +17 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_response.rb +13 -0
- data/generated/lib/runtype/agent_aliases/types/get_v1agents_id_deployments_response_data_item.rb +29 -0
- data/generated/lib/runtype/agent_aliases/types/post_v1agents_id_aliases_alias_rollback_request.rb +21 -0
- data/generated/lib/runtype/agent_aliases/types/post_v1agents_id_aliases_alias_rollback_response.rb +21 -0
- data/generated/lib/runtype/agent_aliases/types/put_v1agents_id_aliases_alias_request.rb +21 -0
- data/generated/lib/runtype/agent_aliases/types/put_v1agents_id_aliases_alias_response.rb +21 -0
- data/generated/lib/runtype/agents/client.rb +14 -2
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response.rb +6 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response_agent_target_resolution.rb +15 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_execution_id_response_agent_version.rb +17 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item.rb +6 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item_agent_target_resolution.rb +15 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_executions_response_data_item_agent_version.rb +17 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_response.rb +2 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_response_sandbox_capability_issues_item.rb +19 -0
- data/generated/lib/runtype/agents/types/get_v1agents_id_response_sandbox_capability_issues_item_code.rb +13 -0
- data/generated/lib/runtype/agents/types/get_v1agents_request.rb +2 -0
- data/generated/lib/runtype/agents/types/get_v1agents_request_managed_in_code.rb +14 -0
- data/generated/lib/runtype/agents/types/get_v1agents_response_data_item.rb +4 -0
- data/generated/lib/runtype/agents/types/get_v1agents_response_data_item_sandbox_capability_issues_item.rb +19 -0
- data/generated/lib/runtype/agents/types/get_v1agents_response_data_item_sandbox_capability_issues_item_code.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request.rb +4 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config.rb +2 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_compaction.rb +23 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_compaction_strategy.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_tool_results.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_context_management_tool_results_prune_mode.rb +14 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice.rb +8 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_endpointing.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_endpointing_strategy.rb +16 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_mode.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_synthesizer.rb +17 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_synthesizer_settings.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_ensure_request_definition_config_voice_speech_transcriber.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config.rb +2 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_compaction.rb +23 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_compaction_strategy.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_tool_results.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_context_management_tool_results_prune_mode.rb +14 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice.rb +8 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_endpointing.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_endpointing_strategy.rb +16 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_mode.rb +15 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_synthesizer.rb +17 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_synthesizer_settings.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_request_config_voice_speech_transcriber.rb +13 -0
- data/generated/lib/runtype/agents/types/post_v1agents_response.rb +2 -0
- data/generated/lib/runtype/agents/types/post_v1agents_response_sandbox_capability_issues_item.rb +19 -0
- data/generated/lib/runtype/agents/types/post_v1agents_response_sandbox_capability_issues_item_code.rb +13 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config.rb +2 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management.rb +13 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_compaction.rb +23 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_compaction_strategy.rb +15 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_tool_results.rb +15 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_context_management_tool_results_prune_mode.rb +14 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice.rb +8 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_endpointing.rb +13 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_endpointing_strategy.rb +16 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech.rb +15 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_mode.rb +15 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_synthesizer.rb +17 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_synthesizer_settings.rb +13 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_request_config_voice_speech_transcriber.rb +13 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_response.rb +2 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_response_sandbox_capability_issues_item.rb +19 -0
- data/generated/lib/runtype/agents/types/put_v1agents_id_response_sandbox_capability_issues_item_code.rb +13 -0
- data/generated/lib/runtype/analytics/types/get_v1analytics_record_results_response_data_data_results_item.rb +4 -0
- data/generated/lib/runtype/client.rb +5 -0
- data/generated/lib/runtype/dispatch/client.rb +11 -4
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent.rb +6 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_durability.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_durability_forced.rb +14 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice.rb +8 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_endpointing.rb +13 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_endpointing_strategy.rb +16 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_mode.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_synthesizer.rb +17 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_synthesizer_settings.rb +13 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_one_agent_voice_speech_transcriber.rb +13 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent.rb +6 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_durability.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_durability_forced.rb +14 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice.rb +8 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_endpointing.rb +13 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_endpointing_strategy.rb +16 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_mode.rb +15 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_synthesizer.rb +17 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_synthesizer_settings.rb +13 -0
- data/generated/lib/runtype/dispatch/types/post_v1dispatch_request_zero_agent_voice_speech_transcriber.rb +13 -0
- data/generated/lib/runtype/evals/types/get_v1eval_id_results_response.rb +2 -0
- data/generated/lib/runtype/evals/types/get_v1eval_id_results_response_case_errors_item.rb +15 -0
- data/generated/lib/runtype/executions/client.rb +53 -0
- data/generated/lib/runtype/executions/types/get_v1executions_execution_id_events_request.rb +15 -0
- data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response.rb +6 -0
- data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response_agent_target_resolution.rb +15 -0
- data/generated/lib/runtype/executions/types/get_v1executions_execution_id_status_response_agent_version.rb +16 -0
- data/generated/lib/runtype/flows/client.rb +2 -1
- data/generated/lib/runtype/flows/types/post_v1flows_ensure_request.rb +2 -0
- data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model.rb +2 -0
- data/generated/lib/runtype/model_configs/types/post_v1model_configs_request_settings_custom_model_reasoning.rb +13 -0
- data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model.rb +2 -0
- data/generated/lib/runtype/model_configs/types/put_v1model_configs_id_request_settings_custom_model_reasoning.rb +13 -0
- data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item.rb +2 -0
- data/generated/lib/runtype/provider_keys/types/post_v1provider_keys_id_sync_models_request_models_item_reasoning.rb +13 -0
- data/generated/lib/runtype/records/types/get_v1records_id_results_response_data_item.rb +4 -0
- data/generated/lib/runtype/tools/client.rb +11 -11
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request.rb +29 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_language.rb +15 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_retention.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy.rb +17 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_idle.rb +11 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_idle_after.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_request_sleep_policy_never.rb +9 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response.rb +2 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy.rb +23 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_metering.rb +14 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_origin.rb +14 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_retention.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy.rb +17 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_idle.rb +11 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_idle_after.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_cf_sandbox_deploy_response_effective_policy_sleep_policy_never.rb +9 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request.rb +35 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_language.rb +15 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_retention.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy.rb +17 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_idle.rb +11 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_idle_after.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_request_sleep_policy_never.rb +9 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response.rb +2 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy.rb +23 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_metering.rb +14 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_origin.rb +14 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_retention.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy.rb +17 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_idle.rb +11 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_idle_after.rb +16 -0
- data/generated/lib/runtype/tools/types/post_v1tools_sandbox_deploy_response_effective_policy_sleep_policy_never.rb +9 -0
- data/generated/lib/runtype/types/agent_ensure_bad_request.rb +11 -0
- data/generated/lib/runtype/types/agent_ensure_bad_request_code.rb +11 -0
- data/generated/lib/runtype/types/agent_ensure_deploy_target.rb +15 -0
- data/generated/lib/runtype/types/agent_ensure_deployment.rb +20 -0
- data/generated/lib/runtype/types/agent_ensure_response_changed_keys.rb +2 -0
- data/generated/lib/runtype/types/agent_ensure_response_changed_keys_deployment.rb +20 -0
- data/generated/lib/runtype/types/agent_ensure_response_version_id.rb +4 -0
- data/generated/lib/runtype/types/agent_ensure_server_error.rb +13 -0
- data/generated/lib/runtype/types/agent_ensure_server_error_code.rb +11 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config.rb +2 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management.rb +11 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_compaction.rb +21 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_compaction_strategy.rb +13 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_tool_results.rb +13 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_context_management_tool_results_prune_mode.rb +12 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice.rb +8 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_endpointing.rb +11 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_endpointing_strategy.rb +14 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech.rb +13 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_mode.rb +13 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_synthesizer.rb +15 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_synthesizer_settings.rb +11 -0
- data/generated/lib/runtype/types/agent_pull_response_definition_config_voice_speech_transcriber.rb +11 -0
- data/generated/lib/runtype/types/dispatch_continuation_json_response.rb +2 -0
- data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response.rb +13 -0
- data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response_pause_kind.rb +12 -0
- data/generated/lib/runtype/types/dispatch_detached_tool_output_json_response_status.rb +11 -0
- data/generated/lib/runtype/types/dispatch_resume_json_response.rb +13 -0
- data/generated/lib/runtype/types/ensure_version_metadata.rb +13 -0
- data/generated/lib/runtype/types/eval_run_scores_response_cases_item.rb +2 -0
- data/generated/lib/runtype/types/execution_stream_event.rb +2 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice.rb +29 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice_kind.rb +11 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice_outcome.rb +14 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice_phase.rb +12 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice_reason.rb +15 -0
- data/generated/lib/runtype/types/execution_stream_event_context_notice_strategy.rb +12 -0
- data/generated/lib/runtype/types/not_found_error_body_code.rb +11 -0
- data/generated/lib/runtype/types/precondition_failed_error_body.rb +17 -0
- data/generated/lib/runtype/types/precondition_failed_error_body_code.rb +11 -0
- data/generated/lib/runtype/types/run_eval_response_cases_item.rb +2 -0
- data/generated/lib/runtype.rb +157 -5
- data/lib/runtype/agent_definition.rb +331 -0
- data/lib/runtype/agent_ensure.rb +110 -0
- data/lib/runtype/canonical_json.rb +89 -0
- data/lib/runtype/client.rb +86 -30
- data/lib/runtype/detached_follow.rb +136 -0
- data/lib/runtype/errors.rb +91 -1
- data/lib/runtype/local_tool_runner.rb +222 -0
- data/lib/runtype/local_tools.rb +258 -0
- data/lib/runtype/sdk_version.rb +1 -1
- data/lib/runtype.rb +7 -1
- 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
|