use std::sync::{Arc, Mutex}; use chrono::Utc; use serde::{Deserialize, Serialize}; use worker_runtime::catalog::{CapabilityRequest, ConfigBundleRef, ProfileSelector}; use worker_runtime::config_bundle::{ ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor, }; use crate::hosts::{ DiagnosticSeverity, RuntimeDiagnostic, RuntimeRegistry, WorkerInputKind, WorkerInputRequest, WorkerOperationState, WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSummary, WorkerTranscriptItem as RuntimeTranscriptItem, WorkerTranscriptProjection, }; const COMPANION_RUNTIME_ID: &str = "embedded-worker-runtime"; const COMPANION_PROFILE_ID: &str = "builtin:companion"; const COMPANION_CONFIG_BUNDLE_ID: &str = "workspace-companion-config"; const MAX_MESSAGE_CHARS: usize = 8_000; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum CompanionState { Ready, Busy, Error, Timeout, Cancelled, Accepted, Rejected, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CompanionStatusResponse { pub state: CompanionState, #[serde(skip_serializing_if = "Option::is_none")] pub worker: Option, pub transport: CompanionTransportSummary, pub diagnostics: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CompanionTransportSummary { pub kind: String, pub completion: String, pub limitation: String, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CompanionMessageRequest { pub content: String, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)] pub struct CompanionCancelRequest { #[serde(default, skip_serializing_if = "Option::is_none")] pub reason: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CompanionMessageResponse { pub state: CompanionState, #[serde(skip_serializing_if = "Option::is_none")] pub worker: Option, #[serde(skip_serializing_if = "Option::is_none")] pub user_item: Option, #[serde(skip_serializing_if = "Option::is_none")] pub assistant_item: Option, pub transcript: CompanionTranscriptProjection, pub diagnostics: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CompanionTranscriptProjection { pub state: CompanionState, pub start: usize, pub limit: usize, pub total_items: usize, #[serde(skip_serializing_if = "Option::is_none")] pub next_start: Option, pub items: Vec, pub diagnostics: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CompanionTranscriptItem { pub sequence: u64, pub role: String, pub content: String, pub created_at: String, pub source: String, pub status: String, } #[derive(Debug)] struct CompanionWorkerState { state: CompanionState, worker: Option, diagnostics: Vec, } pub struct CompanionConsole { runtime: Arc, worker: Mutex, } impl CompanionConsole { pub fn new(runtime: Arc) -> Self { let initial = spawn_companion_worker(&runtime); Self { runtime, worker: Mutex::new(initial), } } pub fn status(&self) -> CompanionStatusResponse { match self.refresh_worker_state() { Ok(worker) => CompanionStatusResponse { state: worker.state, worker: worker.worker.clone(), transport: companion_transport(worker.worker.as_ref()), diagnostics: worker.diagnostics.clone(), }, Err(diagnostic) => CompanionStatusResponse { state: CompanionState::Error, worker: None, transport: companion_transport(None), diagnostics: vec![diagnostic], }, } } pub fn transcript(&self, start: usize, limit: usize) -> CompanionTranscriptProjection { match self.current_worker() { Ok(Some(worker)) => { match self .runtime .transcript(COMPANION_RUNTIME_ID, &worker.worker_id, start, limit) { Ok(transcript) => project_runtime_transcript( &transcript, companion_state_for_worker(&worker), Vec::new(), ), Err(error) => CompanionTranscriptProjection { state: CompanionState::Error, start, limit, total_items: 0, next_start: None, items: Vec::new(), diagnostics: vec![diagnostic( "companion_transcript_unavailable", DiagnosticSeverity::Error, format!("Companion Worker transcript is unavailable: {error:?}"), )], }, } } Ok(None) => CompanionTranscriptProjection { state: CompanionState::Error, start, limit, total_items: 0, next_start: None, items: Vec::new(), diagnostics: vec![diagnostic( "companion_worker_unavailable", DiagnosticSeverity::Error, "Workspace Companion Worker is unavailable", )], }, Err(diagnostic) => CompanionTranscriptProjection { state: CompanionState::Error, start, limit, total_items: 0, next_start: None, items: Vec::new(), diagnostics: vec![diagnostic], }, } } pub fn send_message(&self, request: CompanionMessageRequest) -> CompanionMessageResponse { let content = request.content.trim().to_string(); if content.is_empty() { return self.rejected_message_response(diagnostic( "companion_message_empty", DiagnosticSeverity::Warning, "Companion message content is empty", )); } if content.chars().count() > MAX_MESSAGE_CHARS { return self.rejected_message_response(diagnostic( "companion_message_too_large", DiagnosticSeverity::Warning, format!("Companion message exceeds the {MAX_MESSAGE_CHARS} character limit"), )); } let worker = match self.current_worker() { Ok(Some(worker)) => worker, Ok(None) => { return self.rejected_message_response(diagnostic( "companion_worker_unavailable", DiagnosticSeverity::Error, "Workspace Companion Worker is unavailable", )); } Err(diagnostic) => return self.rejected_message_response(diagnostic), }; let response = self.runtime.send_input( COMPANION_RUNTIME_ID, &worker.worker_id, WorkerInputRequest { kind: WorkerInputKind::User, content: content.clone(), }, ); match response { Ok(result) => { let state = match result.state { WorkerOperationState::Accepted => CompanionState::Accepted, WorkerOperationState::Unsupported | WorkerOperationState::Rejected => { CompanionState::Rejected } }; let diagnostics = if result.diagnostics.is_empty() { Vec::new() } else { result.diagnostics.clone() }; let projection = self.transcript(0, 200); CompanionMessageResponse { state, worker: projection_worker(&self.status()), user_item: projection .items .iter() .rev() .find(|item| item.role == "user" && item.content == content) .cloned(), assistant_item: projection .items .iter() .rev() .find(|item| item.role == "assistant") .cloned(), transcript: projection, diagnostics, } } Err(error) => self.rejected_message_response(diagnostic( "companion_worker_input_failed", DiagnosticSeverity::Error, format!("Companion Worker input dispatch failed: {error:?}"), )), } } pub fn cancel(&self, _request: CompanionCancelRequest) -> CompanionMessageResponse { let diagnostics = vec![diagnostic( "companion_cancel_no_active_run", DiagnosticSeverity::Info, "Workspace Companion has no active generation to cancel", )]; let status = self.status(); let projection = self.transcript(0, 200); CompanionMessageResponse { state: CompanionState::Cancelled, worker: status.worker, user_item: None, assistant_item: projection .items .iter() .rev() .find(|item| item.role == "assistant") .cloned(), transcript: projection, diagnostics, } } fn rejected_message_response(&self, diagnostic: RuntimeDiagnostic) -> CompanionMessageResponse { let status = self.status(); let projection = self.transcript(0, 200); CompanionMessageResponse { state: CompanionState::Rejected, worker: status.worker, user_item: None, assistant_item: projection .items .iter() .rev() .find(|item| item.role == "assistant") .cloned(), transcript: projection, diagnostics: vec![diagnostic], } } fn current_worker(&self) -> Result, RuntimeDiagnostic> { self.refresh_worker_state() .map(|state| state.worker.clone()) } fn refresh_worker_state(&self) -> Result { let mut state = self.worker.lock().map_err(|_| { diagnostic( "companion_state_unavailable", DiagnosticSeverity::Error, "Companion state is unavailable", ) })?; let Some(worker_id) = state.worker.as_ref().map(|worker| worker.worker_id.clone()) else { return Ok(CompanionWorkerState { state: state.state, worker: None, diagnostics: state.diagnostics.clone(), }); }; match self.runtime.worker(COMPANION_RUNTIME_ID, &worker_id) { Ok(worker) => { let mut diagnostics = if worker.capabilities.can_accept_input { Vec::new() } else { state.diagnostics.clone() }; if !worker.capabilities.can_accept_input && !diagnostics .iter() .any(|diagnostic| diagnostic.code == "companion_worker_not_input_capable") { diagnostics.push(companion_not_input_capable_diagnostic(&worker)); } state.state = companion_state_for_worker(&worker); state.worker = Some(worker); state.diagnostics = diagnostics; } Err(error) => { state.state = CompanionState::Error; state.diagnostics = vec![diagnostic( "companion_worker_lookup_failed", DiagnosticSeverity::Error, format!("Companion Worker lookup failed: {error:?}"), )]; } } Ok(CompanionWorkerState { state: state.state, worker: state.worker.clone(), diagnostics: state.diagnostics.clone(), }) } } fn projection_worker(status: &CompanionStatusResponse) -> Option { status.worker.clone() } fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState { let selector = companion_profile_selector(); let mut diagnostics = Vec::new(); let config_bundle = companion_config_bundle(); let config_ref = ConfigBundleRef { id: config_bundle.metadata.id.clone(), digest: config_bundle.metadata.digest.clone(), }; match runtime.sync_config_bundle(COMPANION_RUNTIME_ID, config_bundle) { Ok(result) => diagnostics.extend(result.diagnostics), Err(error) => diagnostics.push(diagnostic( "companion_config_bundle_sync_failed", DiagnosticSeverity::Error, format!("Workspace Companion config bundle sync failed: {error:?}"), )), } let response = runtime.spawn_worker( COMPANION_RUNTIME_ID, WorkerSpawnRequest { intent: WorkerSpawnIntent::WorkspaceCompanion, requested_worker_name: Some("workspace-companion".to_string()), acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, profile: Some(selector), config_bundle: Some(config_ref), requested_capabilities: vec![CapabilityRequest::named("worker.input.user")], }, ); match response { Ok(response) => { diagnostics.extend(response.diagnostics); if let Some(worker) = response.worker { if !worker.capabilities.can_accept_input { diagnostics.push(companion_not_input_capable_diagnostic(&worker)); } CompanionWorkerState { state: companion_state_for_worker(&worker), worker: Some(worker), diagnostics, } } else { diagnostics.push(diagnostic( "companion_worker_missing", DiagnosticSeverity::Error, "Workspace Companion Worker spawn did not return a Worker projection", )); CompanionWorkerState { state: CompanionState::Error, worker: None, diagnostics, } } } Err(error) => CompanionWorkerState { state: CompanionState::Error, worker: None, diagnostics: vec![diagnostic( "companion_worker_spawn_failed", DiagnosticSeverity::Error, format!("Workspace Companion Worker spawn failed: {error:?}"), )], }, } } fn companion_profile_selector() -> ProfileSelector { ProfileSelector::Builtin(COMPANION_PROFILE_ID.to_string()) } fn companion_config_bundle() -> ConfigBundle { ConfigBundle { metadata: ConfigBundleMetadata { id: COMPANION_CONFIG_BUNDLE_ID.to_string(), digest: String::new(), revision: "1".to_string(), workspace_id: "workspace-companion".to_string(), created_at: Utc::now().to_rfc3339(), provenance: ConfigBundleProvenance { source: "workspace-server".to_string(), detail: Some("workspace-companion".to_string()), }, }, profiles: vec![ConfigProfileDescriptor { selector: companion_profile_selector(), label: Some("Workspace Companion".to_string()), }], declarations: Vec::new(), } .with_computed_digest() } fn companion_state_for_worker(worker: &WorkerSummary) -> CompanionState { if !worker.capabilities.can_accept_input { return CompanionState::Error; } match worker.status.as_str() { "busy" | "running" | "stopping" => CompanionState::Busy, "errored" | "error" | "stopped" | "unavailable" => CompanionState::Error, _ => CompanionState::Ready, } } fn companion_not_input_capable_diagnostic(worker: &WorkerSummary) -> RuntimeDiagnostic { diagnostic( "companion_worker_not_input_capable", DiagnosticSeverity::Error, format!( "Workspace Companion Worker '{}' is not input-capable; check profile, provider, secret, and authority diagnostics", worker.worker_id ), ) } fn project_runtime_transcript( transcript: &WorkerTranscriptProjection, state: CompanionState, diagnostics: Vec, ) -> CompanionTranscriptProjection { CompanionTranscriptProjection { state, start: transcript.start, limit: transcript.limit, total_items: transcript.total_items, next_start: transcript.next_start, items: transcript .items .iter() .map(project_runtime_transcript_item) .collect(), diagnostics, } } fn project_runtime_transcript_item(item: &RuntimeTranscriptItem) -> CompanionTranscriptItem { CompanionTranscriptItem { sequence: item.sequence, role: item.role.clone(), content: item.content.clone(), created_at: format!("runtime_sequence:{}", item.sequence), source: "worker_runtime".to_string(), status: "committed".to_string(), } } fn companion_transport(worker: Option<&WorkerSummary>) -> CompanionTransportSummary { if worker.is_some_and(|worker| worker.capabilities.can_accept_input) { CompanionTransportSummary { kind: "embedded_worker_runtime".to_string(), completion: "connected".to_string(), limitation: "Workspace Companion input is dispatched through the normal Worker runtime path." .to_string(), } } else { CompanionTransportSummary { kind: "embedded_worker_runtime".to_string(), completion: "not_input_capable".to_string(), limitation: "Workspace Companion is a Worker but is not input-capable; inspect typed diagnostics for missing profile, provider, secret, or authority." .to_string(), } } } fn diagnostic( code: impl Into, severity: DiagnosticSeverity, message: impl Into, ) -> RuntimeDiagnostic { RuntimeDiagnostic { code: code.into(), severity, message: message.into(), } } #[cfg(test)] mod tests { use super::*; use crate::hosts::{EmbeddedWorkerRuntime, RuntimeRegistry}; use std::collections::HashMap; use std::sync::Mutex as StdMutex; use std::thread; use std::time::{Duration, Instant}; use worker_runtime::execution::{ WorkerExecutionBackend, WorkerExecutionContext, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionRunState, WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult, }; use worker_runtime::identity::WorkerRef; use worker_runtime::interaction::WorkerInput; #[derive(Default)] struct DeterministicExecutionBackend { contexts: StdMutex>, } impl WorkerExecutionBackend for DeterministicExecutionBackend { fn backend_id(&self) -> &str { "deterministic-companion-test" } fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult { self.contexts .lock() .unwrap() .insert(request.worker_ref.clone(), request.context); WorkerExecutionSpawnResult::Connected { handle: WorkerExecutionHandle::new( request.worker_ref.clone(), "deterministic-companion-test", ), run_state: WorkerExecutionRunState::Idle, } } fn dispatch_input( &self, handle: &WorkerExecutionHandle, request: WorkerInput, ) -> WorkerExecutionResult { let worker = handle.worker_ref().clone(); let context = self .contexts .lock() .unwrap() .get(&worker) .cloned() .expect("execution context"); let content = request.content.clone(); thread::spawn(move || { thread::sleep(Duration::from_millis(25)); let _ = context.publish_protocol_event(protocol::Event::TextDone { text: format!("companion echoed: {content}"), }); }); WorkerExecutionResult::accepted( WorkerExecutionOperation::Input, WorkerExecutionRunState::Idle, ) } } #[test] fn companion_spawns_worker_with_companion_profile_and_diagnostic_when_not_input_capable() { let registry = RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test")); let registry = Arc::new(registry); let companion = CompanionConsole::new(registry.clone()); let status = companion.status(); let worker = status.worker.clone().expect("companion worker"); assert_eq!(worker.runtime_id, COMPANION_RUNTIME_ID); assert_eq!(worker.role.as_deref(), Some("workspace_companion")); assert!(!worker.capabilities.can_accept_input); assert_eq!(status.transport.completion, "not_input_capable"); assert!( status .diagnostics .iter() .any(|diagnostic| diagnostic.code == "companion_worker_not_input_capable") ); let response = companion.send_message(CompanionMessageRequest { content: "hello".to_string(), }); assert_eq!(response.state, CompanionState::Rejected); assert!( !response .diagnostics .iter() .any(|diagnostic| diagnostic.code == "companion_llm_not_connected") ); assert!(response.transcript.items.is_empty()); let worker_detail = registry .worker(COMPANION_RUNTIME_ID, &worker.worker_id) .expect("worker detail"); assert_eq!(worker_detail.profile.as_deref(), Some(COMPANION_PROFILE_ID)); let browser_payload = serde_json::to_string(&(status, response, worker_detail)).unwrap(); for forbidden in [ "/workspace/project", "metadata.json", ".jsonl", "/run/user/", "session", "manifest", ] { assert!( !browser_payload.contains(forbidden), "companion projection leaked forbidden term {forbidden}: {browser_payload}" ); } } #[test] fn companion_dispatches_input_and_projects_assistant_output_from_worker_runtime() { let registry = RuntimeRegistry::for_workspace( EmbeddedWorkerRuntime::new_memory_with_execution_backend( "local:test", Arc::new(DeterministicExecutionBackend::default()), ) .expect("embedded runtime"), ); let registry = Arc::new(registry); let companion = CompanionConsole::new(registry.clone()); let status = companion.status(); let worker = status.worker.clone().expect("companion worker"); assert_eq!(status.transport.completion, "connected"); assert_eq!(worker.profile.as_deref(), Some(COMPANION_PROFILE_ID)); assert!(worker.capabilities.can_accept_input); let source = registry .observation_source(COMPANION_RUNTIME_ID, &worker.worker_id) .expect("observation source"); let crate::observation::RuntimeObservationSource::Embedded(source) = source else { panic!("expected embedded observation source"); }; let cursor = source .runtime .worker_observation_cursor_now(&source.worker_ref) .expect("observation cursor"); let response = companion.send_message(CompanionMessageRequest { content: "hello runtime".to_string(), }); assert_eq!(response.state, CompanionState::Accepted); assert!( response .user_item .as_ref() .is_some_and(|item| item.role == "user" && item.content == "hello runtime") ); assert!(response.diagnostics.is_empty()); let deadline = Instant::now() + Duration::from_secs(2); let observed = loop { let observed = source .runtime .read_worker_observation_events(&source.worker_ref, cursor) .expect("observation events"); if observed.iter().any(|event| { serde_json::to_string(event) .unwrap() .contains("companion echoed: hello runtime") }) { break observed; } assert!( Instant::now() < deadline, "timed out waiting for observation event" ); thread::sleep(Duration::from_millis(20)); }; let observed_json = serde_json::to_string(&observed).unwrap(); assert!(observed_json.contains("companion echoed: hello runtime")); let deadline = Instant::now() + Duration::from_secs(2); let transcript = loop { let transcript = companion.transcript(0, 20); if transcript.items.iter().any(|item| { item.role == "assistant" && item.content == "companion echoed: hello runtime" }) { break transcript; } assert!( Instant::now() < deadline, "timed out waiting for companion assistant output: {transcript:?}" ); thread::sleep(Duration::from_millis(20)); }; assert!( transcript .items .iter() .any(|item| item.role == "user" && item.content == "hello runtime") ); assert!(transcript.items.iter().any(|item| { item.role == "assistant" && item.source == "worker_runtime" && item.status == "committed" })); let runtime_transcript = registry .transcript(COMPANION_RUNTIME_ID, &worker.worker_id, 0, 20) .expect("runtime transcript"); assert!(runtime_transcript.items.iter().any(|item| { item.role == "assistant" && item.content == "companion echoed: hello runtime" })); } }