From beb6c3912240958d84e3233390b851dbbccfeb4c Mon Sep 17 00:00:00 2001 From: Vitalii Valkov Date: Tue, 27 Oct 2026 17:18:34 +0100 Subject: [PATCH 1/2] Add workload hibernation to the runner, runners and agents APIs Runner: HibernateWorkload and WakeWorkload (the wake carries fresh image pull credentials), HIBERNATING / HIBERNATED / WAKING in the reported WorkloadStatus, and a typed status plus snapshot_size_bytes on InspectWorkloadResponse and WorkloadListItem. The inspect shape's status was a free-form string and the list item had none; "present when HIBERNATED" needs a status of the enum's type to refer to. Runners: the same three statuses on Workload.status; hibernated_at, woken_at and snapshot_size_bytes on Workload, accepted by UpdateWorkload; snapshot_size_bytes on ReportWorkloadState; WAKE_FAILED and SNAPSHOT_LOST failure reasons; ReportWorkloadIdle on RunnersService and RunnersGateway. Agents: hibernate_after and hibernation_ttl on Agent, Create and Update, shaped like idle_timeout; HIBERNATED on SandboxStatus. Request field names follow their neighbours rather than the spec's prose: the runner-side requests take workload_id like StopWorkloadRequest, and ReportWorkloadIdleRequest takes id like TouchWorkloadRequest. --- proto/agynio/api/agents/v1/agents.proto | 8 ++++++ proto/agynio/api/gateway/v1/runners.proto | 1 + proto/agynio/api/runner/v1/runner.proto | 32 +++++++++++++++++++++++ proto/agynio/api/runners/v1/runners.proto | 31 ++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/proto/agynio/api/agents/v1/agents.proto b/proto/agynio/api/agents/v1/agents.proto index e6dee3f..cef5c9c 100644 --- a/proto/agynio/api/agents/v1/agents.proto +++ b/proto/agynio/api/agents/v1/agents.proto @@ -301,6 +301,8 @@ message Agent { // Vendor model name for native mode, opaque to the platform. Mutually // exclusive with model, which mode the environment is in decides. string model_name = 19; + optional string hibernate_after = 20; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 21; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message CreateAgentRequest { @@ -325,6 +327,8 @@ message CreateAgentRequest { optional string instance_idle_ttl = 17; // Go duration string (e.g., "30m", "6h"). // Required-and-exclusive against model, decided by the environment's llm_mode. string model_name = 18; + optional string hibernate_after = 19; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 20; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message CreateAgentResponse { @@ -371,6 +375,8 @@ message UpdateAgentRequest { optional AgentFinalMessage final_message = 16; optional string instance_idle_ttl = 17; // Go duration string (e.g., "30m", "6h"). optional string model_name = 18; + optional string hibernate_after = 19; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 20; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message UpdateAgentResponse { @@ -770,6 +776,8 @@ enum SandboxStatus { SANDBOX_STATUS_STOPPED = 3; SANDBOX_STATUS_FAILED = 4; SANDBOX_STATUS_TERMINATED = 5; + // Workload state persisted on the runner, occupying no compute. + SANDBOX_STATUS_HIBERNATED = 6; } message Sandbox { diff --git a/proto/agynio/api/gateway/v1/runners.proto b/proto/agynio/api/gateway/v1/runners.proto index bfe2a29..dbe5342 100644 --- a/proto/agynio/api/gateway/v1/runners.proto +++ b/proto/agynio/api/gateway/v1/runners.proto @@ -34,6 +34,7 @@ service RunnersGateway { rpc ListWorkloadsByAgentInstance(agynio.api.runners.v1.ListWorkloadsByAgentInstanceRequest) returns (agynio.api.runners.v1.ListWorkloadsByAgentInstanceResponse); rpc GetWorkload(agynio.api.runners.v1.GetWorkloadRequest) returns (agynio.api.runners.v1.GetWorkloadResponse); rpc TouchWorkload(agynio.api.runners.v1.TouchWorkloadRequest) returns (agynio.api.runners.v1.TouchWorkloadResponse); + rpc ReportWorkloadIdle(agynio.api.runners.v1.ReportWorkloadIdleRequest) returns (agynio.api.runners.v1.ReportWorkloadIdleResponse); rpc StreamWorkloadLogs(agynio.api.runner.v1.StreamWorkloadLogsRequest) returns (stream agynio.api.runner.v1.StreamWorkloadLogsResponse); // --- Volumes --- diff --git a/proto/agynio/api/runner/v1/runner.proto b/proto/agynio/api/runner/v1/runner.proto index 4061e6b..3e17e53 100644 --- a/proto/agynio/api/runner/v1/runner.proto +++ b/proto/agynio/api/runner/v1/runner.proto @@ -15,6 +15,10 @@ service RunnerService { rpc StopWorkload(StopWorkloadRequest) returns (StopWorkloadResponse); rpc RemoveWorkload(RemoveWorkloadRequest) returns (RemoveWorkloadResponse); + // Unimplemented on a runner that does not advertise the hibernate capability. + rpc HibernateWorkload(HibernateWorkloadRequest) returns (HibernateWorkloadResponse); + rpc WakeWorkload(WakeWorkloadRequest) returns (WakeWorkloadResponse); + // Minimal inspect surface with EXACT fields required by platform today. rpc InspectWorkload(InspectWorkloadRequest) returns (InspectWorkloadResponse); @@ -221,6 +225,12 @@ enum WorkloadStatus { WORKLOAD_STATUS_STARTING = 2; WORKLOAD_STATUS_STOPPED = 3; WORKLOAD_STATUS_FAILED = 4; + // Being paused and persisted by the runner. + WORKLOAD_STATUS_HIBERNATING = 5; + // State persisted on the runner, occupying no compute. + WORKLOAD_STATUS_HIBERNATED = 6; + // Being resumed from its persisted state. + WORKLOAD_STATUS_WAKING = 7; } message WorkloadFailure { @@ -244,6 +254,20 @@ message RemoveWorkloadRequest { message RemoveWorkloadResponse {} +message HibernateWorkloadRequest { + string workload_id = 1; +} + +message HibernateWorkloadResponse {} + +message WakeWorkloadRequest { + string workload_id = 1; + + repeated ImagePullCredential image_pull_credentials = 2; +} + +message WakeWorkloadResponse {} + enum ContainerRole { CONTAINER_ROLE_UNSPECIFIED = 0; CONTAINER_ROLE_MAIN = 1; @@ -294,6 +318,11 @@ message InspectWorkloadResponse { string state_status = 7; bool state_running = 8; repeated WorkloadContainer containers = 9; + + WorkloadStatus status = 10; + + // Size of the persisted state in bytes. Present when status is HIBERNATED. + optional int64 snapshot_size_bytes = 11; } message TargetMount { @@ -316,6 +345,9 @@ message ListWorkloadsRequest {} message WorkloadListItem { string instance_id = 1; string workload_key = 2; + WorkloadStatus status = 3; + // Size of the persisted state in bytes. Present when status is HIBERNATED. + optional int64 snapshot_size_bytes = 4; } message ListWorkloadsResponse { diff --git a/proto/agynio/api/runners/v1/runners.proto b/proto/agynio/api/runners/v1/runners.proto index 75b2e07..b076cfc 100644 --- a/proto/agynio/api/runners/v1/runners.proto +++ b/proto/agynio/api/runners/v1/runners.proto @@ -49,6 +49,7 @@ service RunnersService { rpc UpdateWorkloadStatus(UpdateWorkloadStatusRequest) returns (UpdateWorkloadStatusResponse); rpc ReportWorkloadState(ReportWorkloadStateRequest) returns (ReportWorkloadStateResponse); rpc TouchWorkload(TouchWorkloadRequest) returns (TouchWorkloadResponse); + rpc ReportWorkloadIdle(ReportWorkloadIdleRequest) returns (ReportWorkloadIdleResponse); rpc DeleteWorkload(DeleteWorkloadRequest) returns (DeleteWorkloadResponse); rpc GetWorkload(GetWorkloadRequest) returns (GetWorkloadResponse); rpc ListWorkloadsByThread(ListWorkloadsByThreadRequest) returns (ListWorkloadsByThreadResponse); @@ -331,6 +332,12 @@ enum WorkloadStatus { WORKLOAD_STATUS_STOPPING = 3; WORKLOAD_STATUS_STOPPED = 4; WORKLOAD_STATUS_FAILED = 5; + // Being paused and persisted by the runner. + WORKLOAD_STATUS_HIBERNATING = 6; + // State persisted on the runner, occupying no compute. + WORKLOAD_STATUS_HIBERNATED = 7; + // Being resumed from its persisted state. + WORKLOAD_STATUS_WAKING = 8; } enum WorkloadAgentState { @@ -346,6 +353,10 @@ enum WorkloadFailureReason { WORKLOAD_FAILURE_REASON_CONFIG_INVALID = 3; WORKLOAD_FAILURE_REASON_CRASHLOOP = 4; WORKLOAD_FAILURE_REASON_RUNTIME_LOST = 5; + // A hibernated workload could not be woken. + WORKLOAD_FAILURE_REASON_WAKE_FAILED = 6; + // A hibernated workload's persisted state is gone from its runner. + WORKLOAD_FAILURE_REASON_SNAPSHOT_LOST = 7; } enum ContainerRole { @@ -400,6 +411,15 @@ message Workload { // Whether shells in this workload outlive the connections that reach them. // Resolved from the environment at start and fixed for the workload's life. bool persistent_shells = 27; + // When the workload most recently entered WORKLOAD_STATUS_HIBERNATED. Unset + // while it has never been hibernated or is awake. + optional google.protobuf.Timestamp hibernated_at = 28; + // When the workload most recently left WORKLOAD_STATUS_HIBERNATED for + // WORKLOAD_STATUS_RUNNING. Unset until its first wake. + optional google.protobuf.Timestamp woken_at = 29; + // Size of the persisted state in bytes. Set while status is + // WORKLOAD_STATUS_HIBERNATED; unset while the workload is awake. + optional int64 snapshot_size_bytes = 30; // Machine-readable reason for failures when status is WORKLOAD_STATUS_FAILED. // When status is not FAILED, this value may be unset or stale. optional WorkloadFailureReason failure_reason = 15; @@ -460,6 +480,9 @@ message UpdateWorkloadRequest { optional WorkloadFailureReason failure_reason = 7; // Human-readable failure detail; not intended for programmatic parsing. optional string failure_message = 8; + optional google.protobuf.Timestamp hibernated_at = 9; + optional google.protobuf.Timestamp woken_at = 10; + optional int64 snapshot_size_bytes = 11; } message UpdateWorkloadResponse { @@ -473,6 +496,8 @@ message ReportWorkloadStateRequest { WorkloadStatus status = 4; repeated Container containers = 5; google.protobuf.Timestamp observed_at = 6; + // Size of the persisted state in bytes. Set with WORKLOAD_STATUS_HIBERNATED. + optional int64 snapshot_size_bytes = 7; } message ReportWorkloadStateResponse { @@ -497,6 +522,12 @@ message TouchWorkloadRequest { message TouchWorkloadResponse {} +message ReportWorkloadIdleRequest { + string id = 1; +} + +message ReportWorkloadIdleResponse {} + message DeleteWorkloadRequest { string id = 1; } From 8494ed0578ef0ef7e6e460f548c54fd03ace85bd Mon Sep 17 00:00:00 2001 From: Vitalii Valkov Date: Tue, 27 Oct 2026 17:23:18 +0100 Subject: [PATCH 2/2] Add hibernate to Agent The spec decided against a null meaning for hibernate_after and hibernation_ttl. Whether an agent's workloads are hibernated at all is its own boolean, defaulting to true, shaped like Environment.persistent_shells: plain on the read shape, optional on Create and Update so an unset field means the default and "leave it alone" respectively. The two durations stay shaped like idle_timeout. --- proto/agynio/api/agents/v1/agents.proto | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/proto/agynio/api/agents/v1/agents.proto b/proto/agynio/api/agents/v1/agents.proto index cef5c9c..744f75e 100644 --- a/proto/agynio/api/agents/v1/agents.proto +++ b/proto/agynio/api/agents/v1/agents.proto @@ -301,8 +301,10 @@ message Agent { // Vendor model name for native mode, opaque to the platform. Mutually // exclusive with model, which mode the environment is in decides. string model_name = 19; - optional string hibernate_after = 20; // Go duration string (e.g., "0s", "30s", "5m"). - optional string hibernation_ttl = 21; // Go duration string (e.g., "24h", "168h"). Minimum "1h". + // Whether this agent's idle workloads are hibernated. Defaults to true. + bool hibernate = 20; + optional string hibernate_after = 21; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 22; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message CreateAgentRequest { @@ -327,8 +329,10 @@ message CreateAgentRequest { optional string instance_idle_ttl = 17; // Go duration string (e.g., "30m", "6h"). // Required-and-exclusive against model, decided by the environment's llm_mode. string model_name = 18; - optional string hibernate_after = 19; // Go duration string (e.g., "0s", "30s", "5m"). - optional string hibernation_ttl = 20; // Go duration string (e.g., "24h", "168h"). Minimum "1h". + // Unset defaults to true. + optional bool hibernate = 19; + optional string hibernate_after = 20; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 21; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message CreateAgentResponse { @@ -375,8 +379,9 @@ message UpdateAgentRequest { optional AgentFinalMessage final_message = 16; optional string instance_idle_ttl = 17; // Go duration string (e.g., "30m", "6h"). optional string model_name = 18; - optional string hibernate_after = 19; // Go duration string (e.g., "0s", "30s", "5m"). - optional string hibernation_ttl = 20; // Go duration string (e.g., "24h", "168h"). Minimum "1h". + optional bool hibernate = 19; + optional string hibernate_after = 20; // Go duration string (e.g., "0s", "30s", "5m"). + optional string hibernation_ttl = 21; // Go duration string (e.g., "24h", "168h"). Minimum "1h". } message UpdateAgentResponse {