bcode_hello_plugin 0.0.1-alpha.0

Example native plugin for bcode
#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]

//! Example native Bcode plugin.

use bcode_plugin_sdk::prelude::*;

/// Example plugin used by smoke tests.
#[derive(Default)]
pub struct HelloPlugin {
    event_count: usize,
}

fn render_shell_visual(context: &NativeServiceContext) -> Option<ServiceResponse> {
    if context.request.interface_id != bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_INTERFACE_ID
        || context.request.operation != bcode_plugin_sdk::tui_visual::OP_RENDER_TUI_VISUAL
    {
        return None;
    }
    let request = match context
        .request
        .payload_json::<bcode_plugin_sdk::tui_visual::RenderTuiVisualRequest>()
    {
        Ok(request) => request,
        Err(error) => {
            return Some(ServiceResponse::error(
                "invalid_visual_request",
                error.to_string(),
            ));
        }
    };
    Some(
        ServiceResponse::json(&bcode_plugin_sdk::tui_visual::RenderTuiVisualResponse {
            version: bcode_plugin_sdk::tui_visual::TUI_VISUAL_ADAPTER_CONTRACT_VERSION,
            render_mode: "transcript_block".to_owned(),
            anchors: Vec::new(),
            title: Some("Hello user shell".to_owned()),
            timeout_ms: None,
            rows: vec![bcode_plugin_sdk::tui_visual::SerializedTuiRow {
                spans: vec![bcode_plugin_sdk::tui_visual::SerializedTuiSpan {
                    text: format!("hello dynamic {}", request.invocation_id),
                    role: Some(bcode_plugin_sdk::tui_visual::SerializedTuiStyleRole::Accent),
                    foreground: bcode_plugin_sdk::tui_visual::SerializedTuiColor::LightCyan,
                    modifiers: vec![bcode_plugin_sdk::tui_visual::SerializedTuiModifier::Bold],
                }],
            }],
        })
        .unwrap_or_else(|error| {
            ServiceResponse::error("visual_response_encode_failed", error.to_string())
        }),
    )
}

impl RustPlugin for HelloPlugin {
    fn activate(&mut self) -> Result<(), PluginError> {
        Ok(())
    }

    fn register_auth_providers(&mut self, registrar: AuthRegistrar) -> Result<(), PluginError> {
        registrar
            .register(&hello_auth_provider())
            .map_err(|error| PluginError::failed(error.to_string()))
    }

    fn deactivate(&mut self) -> Result<(), PluginError> {
        Ok(())
    }

    fn invoke_service(&mut self, context: NativeServiceContext) -> ServiceResponse {
        if let Some(response) = render_shell_visual(&context) {
            return response;
        }
        if context.request.interface_id == bcode_tool::TOOL_SERVICE_INTERFACE_ID
            && context.request.operation == bcode_tool::OP_PREPARE_TOOL
        {
            return prepare_tool_service_response(
                &context.request,
                [hello_bridge_definition()],
                |_request, _definition| Ok(bcode_plugin_sdk::ToolPolicyPreparation::read_only()),
            );
        }
        if context.request.interface_id == bcode_tool::TOOL_SERVICE_INTERFACE_ID
            && context.request.operation == bcode_tool::OP_INVOKE_TOOL
        {
            let request = match context
                .request
                .payload_json::<bcode_tool::ToolInvocationRequest>()
            {
                Ok(request) => request,
                Err(error) => {
                    return ServiceResponse::error("invalid_tool_request", error.to_string());
                }
            };
            if request.name != "hello_bridge" {
                return ServiceResponse::error("unsupported_tool", request.name);
            }
            return bridge_tool_response(&context, &request.tool_call_id);
        }
        if context.request.interface_id != "example-hello/v1" {
            return ServiceResponse::error(
                "unsupported_interface",
                "unsupported hello service interface",
            );
        }
        match context.request.operation.as_str() {
            "echo" => ServiceResponse::ok(context.request.payload),
            "bridge-exchange" => {
                let request = ServiceBridgeRequest::Exchange(bcode_tool::ToolExchangeRequest {
                    invocation_id: "hello-invocation".to_string(),
                    exchange_id: "hello-exchange".to_string(),
                    producer_id: "example.hello".to_string(),
                    schema: "example.hello.exchange".to_string(),
                    schema_version: 1,
                    payload: serde_json::Value::Null,
                    response_policy: bcode_tool::ToolExchangeResponsePolicy::Required,
                });
                match context.bridge.request(&request) {
                    Ok(response) => ServiceResponse::json(&response).unwrap_or_else(|error| {
                        ServiceResponse::error("bridge_response_encode_failed", error.to_string())
                    }),
                    Err(error) => ServiceResponse::error("bridge_failed", error.to_string()),
                }
            }
            "emit-event" => {
                context.events.emit(b"hello-event");
                ServiceResponse::text("event-emitted")
            }
            "bridge-all" => bridge_responses(&context, "hello-invocation"),
            "wait-cancelled" => {
                if context
                    .cancellation
                    .wait_cancelled(std::time::Duration::from_secs(5))
                {
                    ServiceResponse::text("cancelled")
                } else {
                    ServiceResponse::text("timeout")
                }
            }
            "abort-process" => std::process::abort(),
            "event-count" => ServiceResponse::text(self.event_count.to_string()),
            _ => ServiceResponse::error(
                "unsupported_operation",
                "unsupported hello service operation",
            ),
        }
    }

    fn handle_event(&mut self, context: NativeEventContext) -> Result<(), PluginError> {
        if context.event.topic == "example.event" || context.event.topic == "bcode.session.event" {
            self.event_count += 1;
        }
        Ok(())
    }
}

fn bridge_requests(invocation_id: &str) -> [ServiceBridgeRequest; 4] {
    [
        ServiceBridgeRequest::Exchange(bcode_tool::ToolExchangeRequest {
            invocation_id: invocation_id.to_string(),
            exchange_id: "hello-exchange".to_string(),
            producer_id: "example.hello".to_string(),
            schema: "example.hello.exchange".to_string(),
            schema_version: 1,
            payload: serde_json::Value::Null,
            response_policy: bcode_tool::ToolExchangeResponsePolicy::Required,
        }),
        ServiceBridgeRequest::ReceiveInput {
            invocation_id: invocation_id.to_string(),
            timeout_ms: None,
        },
        ServiceBridgeRequest::InvokeService(bcode_tool::ToolInvocationServiceRequest {
            invocation_id: invocation_id.to_string(),
            request_id: "hello-service".to_string(),
            route_id: None,
            interface_id: "example.nested/v1".to_string(),
            operation: "run".to_string(),
            payload: serde_json::Value::Null,
        }),
        ServiceBridgeRequest::WriteArtifact(bcode_tool::ToolArtifactWriteRequest {
            invocation_id: invocation_id.to_string(),
            artifact_id: "hello-artifact".to_string(),
            content_type: "text/plain".to_string(),
            bytes: b"hello".to_vec(),
            metadata: serde_json::Value::Null,
        }),
    ]
}

fn bridge_responses(context: &NativeServiceContext, invocation_id: &str) -> ServiceResponse {
    let responses = bridge_requests(invocation_id)
        .iter()
        .map(|request| context.bridge.request(request))
        .collect::<Result<Vec<_>, _>>();
    match responses {
        Ok(responses) => ServiceResponse::json(&responses).unwrap_or_else(|error| {
            ServiceResponse::error("bridge_response_encode_failed", error.to_string())
        }),
        Err(error) => ServiceResponse::error("bridge_failed", error.to_string()),
    }
}

fn hello_bridge_definition() -> bcode_tool::ToolDefinition {
    bcode_tool::ToolDefinition {
        name: "hello_bridge".to_string(),
        description: "Exercise every generic invocation capability".to_string(),
        input_schema: serde_json::json!({"type": "object"}),
    }
}

fn bridge_tool_response(context: &NativeServiceContext, invocation_id: &str) -> ServiceResponse {
    let response = bridge_responses(context, invocation_id);
    if let Some(error) = response.error {
        return ServiceResponse {
            payload: response.payload,
            error: Some(error),
        };
    }
    ServiceResponse::json(&bcode_tool::ToolInvocationResponse {
        output: invocation_id.to_string(),
        is_error: false,
        content: Vec::new(),
        full_output: None,
        result: None,
    })
    .unwrap_or_else(|error| {
        ServiceResponse::error("tool_response_encode_failed", error.to_string())
    })
}

fn hello_auth_provider() -> bcode_provider_auth_models::AuthProviderContribution {
    bcode_provider_auth_models::AuthProviderContribution {
        schema_version: bcode_provider_auth_models::AUTH_PROVIDER_CONTRIBUTION_SCHEMA_VERSION,
        provider_id: "example-hello".to_owned(),
        display_name: "Example Hello".to_owned(),
        methods: vec![
            bcode_provider_auth_models::AuthMethodContribution::SecretFields {
                method_id: "api_key".to_owned(),
                display_name: "API key".to_owned(),
                fields: vec![bcode_provider_auth_models::AuthSecretField {
                    invocation_env: Vec::new(),
                    discovery_sources: Vec::new(),
                    credential_id: "api_key".to_owned(),
                    storage_key: "EXAMPLE_HELLO_API_KEY".to_owned(),
                    prompt: "Example API key".to_owned(),
                    optional: false,
                    validation: bcode_provider_auth_models::AuthSecretValidation::default(),
                }],
                supports_verification: false,
                supports_revocation: false,
            },
        ],
    }
}

/// Return the statically linked hello plugin vtable.
#[must_use]
pub fn static_plugin() -> StaticPluginVtable {
    bcode_plugin_sdk::static_plugin_vtable!(HelloPlugin, include_str!("../bcode-plugin.toml"))
}

bcode_plugin_sdk::export_plugin!(HelloPlugin, include_str!("../bcode-plugin.toml"));