agent-infra-sdk 0.2.1

Unified Rust SDK for Gateway-backed and local Agent Infra APIs
Documentation
use super::*;
use async_trait::async_trait;
use std::time::Duration;

#[derive(Clone, Debug)]
pub(super) struct WorkspaceOperationPoller {
    pub(super) transport: HttpTransport,
}

#[async_trait]
impl OperationPoller<WorkspaceOperation> for WorkspaceOperationPoller {
    async fn poll(&self, operation_id: &str) -> Result<WorkspaceOperation, InfraClientError> {
        self.transport
            .get_json(&expand(WORKSPACE_OPERATIONS_PATH, &[operation_id]))
            .await
    }

    async fn cancel(&self, operation_id: &str) -> Result<(), InfraClientError> {
        let operation: WorkspaceOperation = self.poll(operation_id).await?;
        let _: WorkspaceOperation = self
            .transport
            .post_json_with_options(
                &expand(WORKSPACE_OPERATION_CANCEL_PATH, &[operation_id]),
                &WorkspaceActionRequest {
                    expected_version: operation.version,
                    reason: None,
                },
                // Cancellation carries an optimistic version. Until the
                // service stores a cancellation idempotency key, replaying a
                // lost success can manufacture a false version conflict.
                CallOptions::default(),
            )
            .await?;
        Ok(())
    }

    fn observe(&self, operation: &WorkspaceOperation) -> OperationObservation {
        let progress = match operation.state {
            OperationState::Succeeded => OperationProgress::Succeeded,
            OperationState::Failed => OperationProgress::Failed,
            OperationState::Canceled => OperationProgress::Canceled,
            OperationState::Queued | OperationState::Running | OperationState::Canceling => {
                OperationProgress::Pending
            }
        };
        OperationObservation {
            progress,
            next_poll_after: operation.next_poll_after_ms.map(Duration::from_millis),
            error_code: operation.error_code.clone(),
        }
    }
}

pub(super) fn ensure_ack(response: AckResponse) -> Result<Option<String>, InfraClientError> {
    if response.ok {
        Ok(response.revision)
    } else {
        Err(InfraClientError::Protocol {
            service: WORKSPACE_SERVICE_NAME,
            message: "operation returned ok=false".into(),
        })
    }
}

pub(super) fn expand(template: &str, values: &[&str]) -> String {
    let mut result = template.to_string();
    for value in values {
        let Some(start) = result.find('{') else {
            break;
        };
        let Some(relative_end) = result[start..].find('}') else {
            break;
        };
        result.replace_range(start..=start + relative_end, &encode_path_segment(value));
    }
    result
}

pub(super) fn resource_file_path(workspace_id: &str, path: &str) -> String {
    let encoded_path = path
        .trim_start_matches('/')
        .split('/')
        .map(encode_path_segment)
        .collect::<Vec<_>>()
        .join("/");
    expand(WORKSPACE_RESOURCE_FILE_PATH, &[workspace_id]).replace("{*filePath}", &encoded_path)
}

pub(super) fn encode_path_segment(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
            encoded.push(char::from(byte));
        } else {
            use std::fmt::Write as _;
            write!(&mut encoded, "%{byte:02X}").expect("writing to String cannot fail");
        }
    }
    encoded
}