Packages

Elixir SDK for the SW4RM multi-agent coordination protocol

Current section

Files

Jump to
sw4rm_sdk lib sw4rm timeout_profiles.ex
Raw

lib/sw4rm/timeout_profiles.ex

defmodule Sw4rm.TimeoutProfiles do
@moduledoc """
Per-service timeout profiles (SW4-002).
Replaces the universal 30s default with operation-appropriate timeouts.
Provides automatic profile selection, clamping, and override support.
"""
require Logger
@standard_profiles %{
heartbeat: %{
default_timeout_ms: 5_000,
min_timeout_ms: 1_000,
max_timeout_ms: 30_000,
allow_infinite: false
},
registration: %{
default_timeout_ms: 30_000,
min_timeout_ms: 5_000,
max_timeout_ms: 120_000,
allow_infinite: false
},
task_submit: %{
default_timeout_ms: 30_000,
min_timeout_ms: 5_000,
max_timeout_ms: 300_000,
allow_infinite: false
},
message_route: %{
default_timeout_ms: 10_000,
min_timeout_ms: 1_000,
max_timeout_ms: 60_000,
allow_infinite: false
},
negotiation_open: %{
default_timeout_ms: 60_000,
min_timeout_ms: 10_000,
max_timeout_ms: 600_000,
allow_infinite: false
},
negotiation_vote: %{
default_timeout_ms: 300_000,
min_timeout_ms: 30_000,
max_timeout_ms: 1_800_000,
allow_infinite: true
},
negotiation_decision: %{
default_timeout_ms: 60_000,
min_timeout_ms: 5_000,
max_timeout_ms: 300_000,
allow_infinite: false
},
handoff: %{
default_timeout_ms: 60_000,
min_timeout_ms: 10_000,
max_timeout_ms: 300_000,
allow_infinite: false
},
workflow_submit: %{
default_timeout_ms: 30_000,
min_timeout_ms: 5_000,
max_timeout_ms: 120_000,
allow_infinite: false
},
workflow_status: %{
default_timeout_ms: 10_000,
min_timeout_ms: 1_000,
max_timeout_ms: 60_000,
allow_infinite: false
},
tool_call: %{
default_timeout_ms: 30_000,
min_timeout_ms: 1_000,
max_timeout_ms: 3_600_000,
allow_infinite: true
},
hitl_escalate: %{
default_timeout_ms: 300_000,
min_timeout_ms: 60_000,
max_timeout_ms: 3_600_000,
allow_infinite: true
}
}
@doc "Return the profile struct for the given profile name, or nil."
@spec get_profile(atom()) :: map() | nil
def get_profile(name), do: Map.get(@standard_profiles, name)
@doc """
Compute the effective timeout for a profile, applying clamping.
If `override_ms` is nil, returns the profile default.
If `override_ms` is 0 and profile allows infinite, returns 0.
If `override_ms` is 0 and profile does not allow infinite, returns default and logs warning.
Otherwise clamps to [min, max] and logs warning if clamped.
"""
@spec effective_timeout(atom(), non_neg_integer() | nil) :: non_neg_integer()
def effective_timeout(profile_name, override_ms \\ nil) do
case get_profile(profile_name) do
nil ->
override_ms || Sw4rm.Constants.default_timeout_ms()
profile ->
compute_effective(profile_name, profile, override_ms)
end
end
defp compute_effective(_name, profile, nil), do: profile.default_timeout_ms
defp compute_effective(name, profile, 0) do
if profile.allow_infinite do
0
else
Logger.warning(
"SW4-002: timeout=0 (infinite) not allowed for profile #{name}, using default #{profile.default_timeout_ms}ms"
)
profile.default_timeout_ms
end
end
defp compute_effective(name, profile, override) do
cond do
override < profile.min_timeout_ms ->
Logger.warning(
"SW4-002: timeout #{override}ms below min for profile #{name}, clamping to #{profile.min_timeout_ms}ms"
)
profile.min_timeout_ms
override > profile.max_timeout_ms ->
Logger.warning(
"SW4-002: timeout #{override}ms above max for profile #{name}, clamping to #{profile.max_timeout_ms}ms"
)
profile.max_timeout_ms
true ->
override
end
end
# Service/method → profile mappings (spec §3)
@rpc_profiles %{
# Registry
{:registry, :register_agent} => :registration,
{:registry, :heartbeat} => :heartbeat,
{:registry, :deregister_agent} => :registration,
# Router
{:router, :send_message} => :message_route,
{:router, :stream_incoming} => :message_route,
# Scheduler
{:scheduler, :submit_task} => :task_submit,
{:scheduler, :request_preemption} => :task_submit,
{:scheduler, :shutdown_agent} => :task_submit,
{:scheduler, :poll_activity_buffer} => :workflow_status,
{:scheduler, :purge_activity} => :task_submit,
# Negotiation
{:negotiation, :open} => :negotiation_open,
{:negotiation, :propose} => :negotiation_open,
{:negotiation, :counter} => :negotiation_open,
{:negotiation, :evaluate} => :negotiation_vote,
{:negotiation, :decide} => :negotiation_decision,
{:negotiation, :abort} => :negotiation_decision,
# NegotiationRoom
{:negotiation_room, :submit_proposal} => :negotiation_open,
{:negotiation_room, :submit_vote} => :negotiation_vote,
{:negotiation_room, :get_votes} => :negotiation_decision,
{:negotiation_room, :get_decision} => :negotiation_decision,
{:negotiation_room, :wait_for_decision} => :negotiation_vote,
# Handoff
{:handoff, :request_handoff} => :handoff,
{:handoff, :accept_handoff} => :handoff,
{:handoff, :reject_handoff} => :message_route,
{:handoff, :get_pending_handoffs} => :workflow_status,
{:handoff, :complete_handoff} => :handoff,
{:handoff, :cancel_delegation} => :handoff,
# Tool
{:tool, :call} => :tool_call,
{:tool, :call_stream} => :tool_call,
{:tool, :cancel} => :message_route,
# HITL
{:hitl, :decide} => :hitl_escalate,
# Workflow
{:workflow, :create_workflow} => :workflow_submit,
{:workflow, :start_workflow} => :workflow_submit,
{:workflow, :get_workflow_state} => :workflow_status,
{:workflow, :resume_workflow} => :workflow_submit,
# Worktree
{:worktree, :bind} => :registration,
{:worktree, :unbind} => :registration,
{:worktree, :request_switch} => :handoff,
{:worktree, :approve_switch} => :handoff,
{:worktree, :reject_switch} => :message_route,
{:worktree, :status} => :workflow_status,
# Connector
{:connector, :register_provider} => :registration,
{:connector, :describe_tools} => :message_route,
# Reasoning
{:reasoning, :check_parallelism} => :message_route,
{:reasoning, :evaluate_debate} => :negotiation_vote,
{:reasoning, :summarize} => :tool_call,
# Activity
{:activity, :append_artifact} => :task_submit,
{:activity, :list_artifacts} => :workflow_status,
# Logging
{:logging, :ingest} => :message_route,
# SchedulerPolicy
{:scheduler_policy, :set_negotiation_policy} => :task_submit,
{:scheduler_policy, :get_negotiation_policy} => :workflow_status,
{:scheduler_policy, :set_policy_profiles} => :task_submit,
{:scheduler_policy, :list_policy_profiles} => :workflow_status,
{:scheduler_policy, :get_effective_policy} => :workflow_status,
{:scheduler_policy, :submit_evaluation} => :task_submit,
{:scheduler_policy, :hitl_action} => :hitl_escalate
}
@doc "Return the timeout profile name for a given service/method pair."
@spec profile_for_rpc(atom(), atom()) :: atom() | nil
def profile_for_rpc(service, method) do
Map.get(@rpc_profiles, {service, method})
end
end