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
data/lib/runtype/client.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
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
|
-
#
|
|
257
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
304
|
+
rewrite_body_key(body, :record) do |record|
|
|
305
|
+
next record unless record.is_a?(Hash)
|
|
268
306
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
data/lib/runtype/errors.rb
CHANGED
|
@@ -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
|