runtime: remove fake companion responses

This commit is contained in:
Keisuke Hirata 2026-06-28 03:06:12 +09:00
parent 67df7d1a53
commit 2fb7514daa
No known key found for this signature in database
6 changed files with 77 additions and 343 deletions

View File

@ -960,7 +960,7 @@ mod tests {
.await;
assert_eq!(response.status(), StatusCode::OK);
let detail: RuntimeHttpWorkerResponse = read_json(response).await;
assert_eq!(detail.worker.transcript_len, 2);
assert_eq!(detail.worker.transcript_len, 1);
let response = empty_request(
app.clone(),
@ -999,7 +999,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let workers: RuntimeHttpWorkersResponse = read_json(response).await;
assert_eq!(workers.workers.len(), 1);
assert_eq!(workers.workers[0].transcript_len, 2);
assert_eq!(workers.workers[0].transcript_len, 1);
let response = empty_request(app, Method::GET, "/v1/runtime").await;
assert_eq!(response.status(), StatusCode::OK);

View File

@ -33,10 +33,6 @@ use tokio::sync::broadcast;
static NEXT_RUNTIME_SEQUENCE: AtomicU64 = AtomicU64::new(1);
fn providerless_embedded_response_text() -> &'static str {
"Embedded worker-runtime accepted the message. LLM execution is not connected for this worker yet."
}
/// Concrete embedded Runtime domain entity.
///
/// The default implementation is memory-backed and tools/provider-less by
@ -319,36 +315,10 @@ impl Runtime {
};
state.push_worker_observation_event(worker_ref.clone(), payload);
}
let assistant_transcript_sequence = if matches!(role, TranscriptRole::User) {
let assistant_text = providerless_embedded_response_text().to_string();
let worker = state.worker_mut(worker_ref)?;
let assistant_sequence = worker.next_transcript_sequence;
worker.next_transcript_sequence += 1;
worker.transcript.push(TranscriptEntry {
sequence: assistant_sequence,
worker_ref: worker_ref.clone(),
role: TranscriptRole::Assistant,
content: assistant_text.clone(),
event_id,
});
#[cfg(feature = "ws-server")]
state.push_worker_observation_event(
worker_ref.clone(),
protocol::Event::TextDone {
text: assistant_text,
},
);
Some(assistant_sequence)
} else {
None
};
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
state.persist_event_by_id(event_id)?;
state.persist_transcript_entry(&worker_ref.worker_id, transcript_sequence)?;
if let Some(sequence) = assistant_transcript_sequence {
state.persist_transcript_entry(&worker_ref.worker_id, sequence)?;
}
Ok(WorkerInteractionAck {
worker_ref: worker_ref.clone(),
@ -1412,10 +1382,10 @@ mod tests {
let projection = runtime
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 2))
.unwrap();
assert_eq!(projection.total_items, 5);
assert_eq!(projection.total_items, 3);
assert_eq!(projection.items.len(), 2);
assert_eq!(projection.items[0].content, "hello");
assert_eq!(projection.items[1].role, TranscriptRole::Assistant);
assert_eq!(projection.items[1].role, TranscriptRole::System);
assert_eq!(projection.next_start, Some(2));
let err = runtime

View File

@ -1,19 +1,15 @@
use std::sync::{Arc, Mutex};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use worker_runtime::catalog::{CapabilityRequest, ProfileSelector};
use crate::hosts::{
DiagnosticSeverity, RuntimeDiagnostic, RuntimeRegistry, WorkerInputKind, WorkerInputRequest,
WorkerOperationState, WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest,
WorkerSummary,
DiagnosticSeverity, RuntimeDiagnostic, RuntimeRegistry, WorkerOperationState,
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSummary,
};
const COMPANION_RUNTIME_ID: &str = "embedded-worker-runtime";
const MAX_MESSAGE_CHARS: usize = 8_000;
const PROVIDERLESS_RESPONSE: &str =
include_str!("../../../resources/prompts/worker/web_companion_providerless.md");
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
@ -103,7 +99,6 @@ struct CompanionWorkerState {
}
pub struct CompanionConsole {
runtime: Arc<RuntimeRegistry>,
worker: Mutex<CompanionWorkerState>,
transcript: Mutex<CompanionTranscript>,
}
@ -112,7 +107,6 @@ impl CompanionConsole {
pub fn new(runtime: Arc<RuntimeRegistry>) -> Self {
let initial = spawn_companion_worker(&runtime);
Self {
runtime,
worker: Mutex::new(initial),
transcript: Mutex::new(CompanionTranscript::default()),
}
@ -125,7 +119,7 @@ impl CompanionConsole {
return CompanionStatusResponse {
state: CompanionState::Error,
worker: None,
transport: providerless_transport(),
transport: companion_transport(),
diagnostics: vec![diagnostic(
"companion_state_unavailable",
DiagnosticSeverity::Error,
@ -137,7 +131,7 @@ impl CompanionConsole {
CompanionStatusResponse {
state: worker.state,
worker: worker.worker.clone(),
transport: providerless_transport(),
transport: companion_transport(),
diagnostics: worker.diagnostics.clone(),
}
}
@ -181,125 +175,18 @@ impl CompanionConsole {
));
}
let mut transcript = match self.transcript.try_lock() {
Ok(transcript) => transcript,
Err(std::sync::TryLockError::WouldBlock) => {
return self.busy_message_response();
}
Err(std::sync::TryLockError::Poisoned(_)) => {
return self.error_message_response(diagnostic(
"companion_transcript_unavailable",
DiagnosticSeverity::Error,
"Companion transcript is unavailable",
));
}
};
let (worker, mut diagnostics) = match self.current_worker() {
Ok((Some(worker), diagnostics)) => (worker, diagnostics),
Ok((None, diagnostics)) => {
return response_from_locked_transcript(
&transcript,
CompanionState::Error,
None,
None,
None,
0,
200,
diagnostics,
);
}
Err(diagnostic) => {
return response_from_locked_transcript(
&transcript,
CompanionState::Error,
None,
None,
None,
0,
200,
vec![diagnostic],
);
}
};
let user_item = transcript.push("user", content.clone(), "browser_request", "accepted");
match self.runtime.send_input(
&worker.runtime_id,
&worker.worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content,
},
) {
Ok(result) if result.state == WorkerOperationState::Accepted => {
diagnostics.extend(result.diagnostics);
}
Ok(result) => {
diagnostics.extend(result.diagnostics);
diagnostics.push(diagnostic(
"companion_runtime_input_rejected",
DiagnosticSeverity::Error,
"Embedded Companion Worker rejected the browser message",
));
return response_from_locked_transcript(
&transcript,
CompanionState::Error,
Some(worker),
Some(user_item),
None,
0,
200,
diagnostics,
);
}
Err(error) => {
diagnostics.push(diagnostic(
"companion_runtime_input_failed",
DiagnosticSeverity::Error,
format!("Embedded Companion Worker input failed: {error:?}"),
));
return response_from_locked_transcript(
&transcript,
CompanionState::Error,
Some(worker),
Some(user_item),
None,
0,
200,
diagnostics,
);
}
}
diagnostics.push(diagnostic(
"companion_providerless_boundary",
DiagnosticSeverity::Info,
"Real LLM completion is not connected in this MVP; response is the backend provider-less boundary text",
));
let assistant_item = transcript.push(
"assistant",
providerless_response_text(),
"backend_providerless_boundary",
"complete",
);
response_from_locked_transcript(
&transcript,
CompanionState::Accepted,
Some(worker),
Some(user_item),
Some(assistant_item),
0,
200,
diagnostics,
)
self.rejected_message_response(diagnostic(
"companion_llm_not_connected",
DiagnosticSeverity::Error,
"Workspace Companion input is disabled until it is connected to actual Worker/LLM execution",
))
}
pub fn cancel(&self, _request: CompanionCancelRequest) -> CompanionMessageResponse {
let diagnostics = vec![diagnostic(
"companion_cancel_no_active_run",
DiagnosticSeverity::Info,
"Provider-less Companion Console has no active generation to cancel",
"Workspace Companion has no active generation to cancel",
)];
match self.transcript.lock() {
Ok(transcript) => response_from_locked_transcript(
@ -335,19 +222,6 @@ impl CompanionConsole {
}
}
fn current_worker(
&self,
) -> Result<(Option<WorkerSummary>, Vec<RuntimeDiagnostic>), RuntimeDiagnostic> {
let worker = self.worker.lock().map_err(|_| {
diagnostic(
"companion_state_unavailable",
DiagnosticSeverity::Error,
"Companion state is unavailable",
)
})?;
Ok((worker.worker.clone(), worker.diagnostics.clone()))
}
fn rejected_message_response(&self, diagnostic: RuntimeDiagnostic) -> CompanionMessageResponse {
match self.transcript.lock() {
Ok(transcript) => response_from_locked_transcript(
@ -378,83 +252,6 @@ impl CompanionConsole {
},
}
}
fn busy_message_response(&self) -> CompanionMessageResponse {
let diagnostic = diagnostic(
"companion_busy",
DiagnosticSeverity::Warning,
"Companion Console is already processing a message",
);
match self.transcript.lock() {
Ok(transcript) => response_from_locked_transcript(
&transcript,
CompanionState::Busy,
self.status().worker,
None,
None,
0,
200,
vec![diagnostic],
),
Err(_) => CompanionMessageResponse {
state: CompanionState::Busy,
worker: self.status().worker,
user_item: None,
assistant_item: None,
transcript: CompanionTranscriptProjection {
state: CompanionState::Busy,
start: 0,
limit: 200,
total_items: 0,
next_start: None,
items: Vec::new(),
diagnostics: vec![diagnostic.clone()],
},
diagnostics: vec![diagnostic],
},
}
}
fn error_message_response(&self, diagnostic: RuntimeDiagnostic) -> CompanionMessageResponse {
CompanionMessageResponse {
state: CompanionState::Error,
worker: self.status().worker,
user_item: None,
assistant_item: None,
transcript: CompanionTranscriptProjection {
state: CompanionState::Error,
start: 0,
limit: 200,
total_items: 0,
next_start: None,
items: Vec::new(),
diagnostics: vec![diagnostic.clone()],
},
diagnostics: vec![diagnostic],
}
}
}
impl CompanionTranscript {
fn push(
&mut self,
role: impl Into<String>,
content: impl Into<String>,
source: impl Into<String>,
status: impl Into<String>,
) -> CompanionTranscriptItem {
self.next_sequence = self.next_sequence.saturating_add(1);
let item = CompanionTranscriptItem {
sequence: self.next_sequence,
role: role.into(),
content: content.into(),
created_at: Utc::now().to_rfc3339(),
source: source.into(),
status: status.into(),
};
self.items.push(item.clone());
item
}
}
fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState {
@ -537,15 +334,11 @@ fn project_transcript(
}
}
fn providerless_response_text() -> String {
PROVIDERLESS_RESPONSE.trim().to_string()
}
fn providerless_transport() -> CompanionTransportSummary {
fn companion_transport() -> CompanionTransportSummary {
CompanionTransportSummary {
kind: "providerless_backend_internal".to_string(),
completion: "synchronous_request_response".to_string(),
limitation: "No provider-backed LLM generation is wired in this MVP; browser messages are recorded by a backend-internal tools-less Companion Worker and receive a resource-defined boundary response.".to_string(),
kind: "embedded_worker_runtime".to_string(),
completion: "not_connected".to_string(),
limitation: "Workspace Companion is visible as an embedded Worker, but browser input is disabled until actual Worker/LLM execution is connected.".to_string(),
}
}
@ -567,7 +360,7 @@ mod tests {
use crate::hosts::{EmbeddedWorkerRuntime, RuntimeRegistry};
#[test]
fn companion_spawns_visible_worker_and_records_providerless_turn() {
fn companion_spawns_visible_worker_without_fake_turn() {
let registry =
RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test"));
let registry = Arc::new(registry);
@ -591,22 +384,19 @@ mod tests {
let response = companion.send_message(CompanionMessageRequest {
content: "hello".to_string(),
});
assert_eq!(response.state, CompanionState::Accepted);
assert_eq!(response.transcript.items.len(), 2);
assert_eq!(response.transcript.items[0].role, "user");
assert_eq!(response.transcript.items[1].role, "assistant");
assert_eq!(response.state, CompanionState::Rejected);
assert!(response.transcript.items.is_empty());
assert!(
response.transcript.items[1]
.content
.contains("provider-less")
response
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "companion_llm_not_connected")
);
let runtime_transcript = registry
.transcript(COMPANION_RUNTIME_ID, &worker.worker_id, 0, 10)
.unwrap();
assert_eq!(runtime_transcript.items.len(), 2);
assert_eq!(runtime_transcript.items[0].role, "user");
assert_eq!(runtime_transcript.items[1].role, "assistant");
assert!(runtime_transcript.items.is_empty());
let browser_payload = serde_json::to_string(&(status, response)).unwrap();
for forbidden in [

View File

@ -957,7 +957,7 @@ impl EmbeddedWorkerRuntime {
display_hint: "backend-internal worker-runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_accept_input: true,
can_accept_input: false,
can_stop: false,
can_spawn_followup: false,
},
@ -989,7 +989,7 @@ impl EmbeddedWorkerRuntime {
display_hint: "backend-internal worker-runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_accept_input: true,
can_accept_input: false,
can_stop: false,
can_spawn_followup: false,
},
@ -1255,40 +1255,17 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
))
}
fn send_input(&self, worker_id: &str, request: WorkerInputRequest) -> WorkerInputResult {
let Some(worker_ref) = self.worker_ref(worker_id) else {
return embedded_input_rejected(
&self.runtime_id,
worker_id,
diagnostic(
"embedded_worker_id_invalid",
DiagnosticSeverity::Warning,
"Worker id was empty and cannot be resolved".to_string(),
),
);
};
let input = EmbeddedWorkerInput {
kind: match request.kind {
WorkerInputKind::User => EmbeddedWorkerInputKind::User,
WorkerInputKind::System => EmbeddedWorkerInputKind::System,
},
content: request.content,
};
match self.runtime.send_input(&worker_ref, input) {
Ok(ack) => WorkerInputResult {
state: WorkerOperationState::Accepted,
runtime_id: self.runtime_id.clone(),
worker_id: worker_id.to_string(),
transcript_sequence: Some(ack.transcript_sequence),
event_id: Some(ack.event_id),
diagnostics: Vec::new(),
},
Err(err) => embedded_input_rejected(
&self.runtime_id,
worker_id,
embedded_runtime_diagnostic(&err),
fn send_input(&self, worker_id: &str, _request: WorkerInputRequest) -> WorkerInputResult {
embedded_input_rejected(
&self.runtime_id,
worker_id,
diagnostic(
"embedded_worker_llm_not_connected",
DiagnosticSeverity::Error,
"Embedded Worker input is disabled until actual Worker/LLM execution is connected"
.to_string(),
),
}
)
}
fn transcript(
@ -1878,7 +1855,7 @@ fn embedded_runtime_capabilities(limit: usize, available: bool) -> RuntimeCapabi
can_get_worker: available,
can_spawn_worker: available,
can_stop_worker: false,
can_accept_input: available,
can_accept_input: false,
has_workspace_fs: false,
has_shell: false,
has_git: false,
@ -2611,7 +2588,7 @@ mod tests {
);
assert_eq!(embedded_summary.source.status, RuntimeSourceStatus::Active);
assert!(embedded_summary.capabilities.can_spawn_worker);
assert!(embedded_summary.capabilities.can_accept_input);
assert!(!embedded_summary.capabilities.can_accept_input);
let spawned = registry
.spawn_worker(
@ -2644,7 +2621,7 @@ mod tests {
assert_eq!(worker.workspace.identity, "runtime_registry_worker");
assert_eq!(worker.implementation.kind, "embedded_worker_runtime");
assert_eq!(worker.profile.as_deref(), Some("builtin:coder"));
assert!(worker.capabilities.can_accept_input);
assert!(!worker.capabilities.can_accept_input);
let input = registry
.send_input(
@ -2656,24 +2633,21 @@ mod tests {
},
)
.unwrap();
assert_eq!(input.state, WorkerOperationState::Accepted);
assert_eq!(input.state, WorkerOperationState::Rejected);
assert_eq!(input.runtime_id, EMBEDDED_RUNTIME_ID);
assert_eq!(input.worker_id, worker.worker_id);
assert_eq!(input.transcript_sequence, Some(1));
assert!(
input
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "embedded_worker_llm_not_connected")
);
let transcript = registry
.transcript(EMBEDDED_RUNTIME_ID, &worker.worker_id, 0, 10)
.unwrap();
assert_eq!(transcript.state, WorkerOperationState::Accepted);
assert_eq!(transcript.items.len(), 2);
assert_eq!(transcript.items[0].role, "user");
assert_eq!(transcript.items[0].content, "hello embedded runtime");
assert_eq!(transcript.items[1].role, "assistant");
assert!(
transcript.items[1]
.content
.contains("LLM execution is not connected")
);
assert!(transcript.items.is_empty());
let json = serde_json::to_string(&(embedded_summary, worker, transcript)).unwrap();
for forbidden in [

View File

@ -327,16 +327,16 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
extension_points: ExtensionPoints {
store: "sqlite".to_string(),
event_stream: ExtensionPointState {
status: "reserved".to_string(),
note: "No browser-to-Worker socket path is exposed in this bootstrap; any future stream must be a Workspace server proxy that resolves Worker identity and enforces method allow/block boundaries.".to_string(),
status: "backend_proxy".to_string(),
note: "Worker observation streams are exposed only through the Workspace server proxy keyed by runtime_id + worker_id; browser clients never receive raw Runtime endpoints or socket paths.".to_string(),
},
host_worker_bridge: ExtensionPointState {
status: "read_only_local".to_string(),
note: "Local Hosts and Workers are exposed as a read-only bridge over existing Worker metadata; no direct Worker socket, scheduling, or lifecycle control is implemented.".to_string(),
status: "runtime_registry".to_string(),
note: "Hosts and Workers are projected from the Workspace RuntimeRegistry; raw Runtime endpoints, sockets, and local metadata paths are not exposed.".to_string(),
},
companion_console: ExtensionPointState {
status: "providerless_mvp".to_string(),
note: "Backend-internal tools-less Companion Worker is available through Workspace API status/transcript/message endpoints; v0 records browser messages and returns a resource-defined provider-less response instead of direct LLM generation.".to_string(),
status: "not_connected".to_string(),
note: "Workspace Companion is visible as an embedded Worker, but browser input is disabled until actual Worker/LLM execution is connected.".to_string(),
},
},
}))
@ -1051,7 +1051,7 @@ mod tests {
assert_eq!(workspace["record_authority"], "local_yoi_project_records");
assert_eq!(
workspace["extension_points"]["host_worker_bridge"]["status"],
"read_only_local"
"runtime_registry"
);
let tickets = get_json(app.clone(), "/api/tickets").await;
@ -1141,7 +1141,7 @@ mod tests {
assert_eq!(companion_status["worker"]["role"], "workspace_companion");
assert_eq!(
companion_status["transport"]["kind"],
"providerless_backend_internal"
"embedded_worker_runtime"
);
assert!(!companion_status.to_string().contains("/workspace/demo"));
@ -1151,23 +1151,24 @@ mod tests {
json!({ "content": "hello companion" }),
)
.await;
assert_eq!(companion_message["state"], "accepted");
assert_eq!(companion_message["transcript"]["items"][0]["role"], "user");
assert_eq!(
companion_message["transcript"]["items"][1]["role"],
"assistant"
assert_eq!(companion_message["state"], "rejected");
assert!(
companion_message["transcript"]["items"]
.as_array()
.unwrap()
.is_empty()
);
assert!(
companion_message["transcript"]["items"][1]["content"]
.as_str()
companion_message["diagnostics"]
.as_array()
.unwrap()
.contains("provider-less")
.iter()
.any(|diagnostic| diagnostic["code"] == "companion_llm_not_connected")
);
assert!(!companion_message.to_string().contains("/workspace/demo"));
let companion_transcript = get_json(app.clone(), "/api/companion/transcript").await;
assert_eq!(companion_transcript["total_items"], 2);
assert_eq!(companion_transcript["items"][1]["role"], "assistant");
assert_eq!(companion_transcript["total_items"], 0);
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
assert!(
@ -1328,10 +1329,16 @@ mod tests {
}),
)
.await;
assert_eq!(accepted["state"], "accepted");
assert_eq!(accepted["state"], "rejected");
assert_eq!(accepted["runtime_id"], "embedded-worker-runtime");
assert_eq!(accepted["worker_id"], worker_id);
assert_eq!(accepted["transcript_sequence"], 1);
assert!(
accepted["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "embedded_worker_llm_not_connected")
);
let transcript = get_json(
app.clone(),
@ -1339,11 +1346,7 @@ mod tests {
)
.await;
assert_eq!(transcript["state"], "accepted");
assert_eq!(transcript["items"][0]["role"], "user");
assert_eq!(
transcript["items"][0]["content"],
"hello from browser-facing api"
);
assert!(transcript["items"].as_array().unwrap().is_empty());
let wrong_runtime = app
.clone()

View File

@ -1,3 +0,0 @@
You are connected to the Yoi Workspace Web Console MVP provider-less boundary.
I received your browser message through the backend-internal Companion Worker, but this MVP does not yet run a provider-backed LLM completion from the workspace server. The transcript/status/send path is active, tools-less, and scoped to conversation projection only. A later integration can replace this resource-defined boundary response with real Worker engine output without giving the browser runtime credentials, sockets, session paths, or filesystem authority.