feat: add web console commands
This commit is contained in:
@@ -398,6 +398,15 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static {
|
||||
fn worker_snapshot(&self, _handle: &WorkerExecutionHandle) -> Option<protocol::Event> {
|
||||
None
|
||||
}
|
||||
|
||||
fn worker_completions(
|
||||
&self,
|
||||
_handle: &WorkerExecutionHandle,
|
||||
_kind: protocol::CompletionKind,
|
||||
_prefix: &str,
|
||||
) -> Vec<protocol::CompletionEntry> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -485,6 +494,15 @@ impl WorkerExecutionBackendRef {
|
||||
) -> Option<protocol::Event> {
|
||||
self.backend.worker_snapshot(handle)
|
||||
}
|
||||
|
||||
pub(crate) fn worker_completions(
|
||||
&self,
|
||||
handle: &WorkerExecutionHandle,
|
||||
kind: protocol::CompletionKind,
|
||||
prefix: &str,
|
||||
) -> Vec<protocol::CompletionEntry> {
|
||||
self.backend.worker_completions(handle, kind, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for WorkerExecutionBackendRef {
|
||||
|
||||
@@ -146,6 +146,10 @@ pub fn runtime_http_router(runtime: Runtime, local_token: Option<String>) -> Rou
|
||||
get(get_worker).delete(delete_worker),
|
||||
)
|
||||
.route("/v1/workers/{worker_id}/input", post(send_worker_input))
|
||||
.route(
|
||||
"/v1/workers/{worker_id}/completions",
|
||||
post(worker_completions),
|
||||
)
|
||||
.route("/v1/workers/{worker_id}/stop", post(stop_worker))
|
||||
.route("/v1/workers/{worker_id}/cancel", post(cancel_worker));
|
||||
|
||||
@@ -228,6 +232,20 @@ pub struct RuntimeHttpWorkerInputResponse {
|
||||
pub ack: WorkerInteractionAck,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkerCompletionsRequest {
|
||||
pub kind: protocol::CompletionKind,
|
||||
#[serde(default)]
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkerCompletionsResponse {
|
||||
pub kind: protocol::CompletionKind,
|
||||
pub prefix: String,
|
||||
pub entries: Vec<protocol::CompletionEntry>,
|
||||
}
|
||||
|
||||
/// Worker lifecycle request body used by stop/cancel endpoints.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkerLifecycleRequest {
|
||||
@@ -678,6 +696,24 @@ async fn send_worker_input(
|
||||
Ok(Json(RuntimeHttpWorkerInputResponse { ack }))
|
||||
}
|
||||
|
||||
async fn worker_completions(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(worker_id): Path<String>,
|
||||
body: Result<Json<RuntimeHttpWorkerCompletionsRequest>, JsonRejection>,
|
||||
) -> RestResult<RuntimeHttpWorkerCompletionsResponse> {
|
||||
let worker_ref = worker_ref_for(&state.runtime, worker_id)?;
|
||||
let Json(request) = body.map_err(RuntimeHttpRestError::json_rejection)?;
|
||||
let entries = state
|
||||
.runtime
|
||||
.worker_completions(&worker_ref, request.kind, &request.prefix)
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpWorkerCompletionsResponse {
|
||||
kind: request.kind,
|
||||
prefix: request.prefix,
|
||||
entries,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn stop_worker(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(worker_id): Path<String>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::catalog::WorkerStatus;
|
||||
use crate::identity::WorkerRef;
|
||||
use protocol::Segment;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input kind accepted by the embedded interaction API.
|
||||
@@ -8,6 +9,15 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum WorkerInputKind {
|
||||
User,
|
||||
System,
|
||||
Compact,
|
||||
ListRewindTargets,
|
||||
RegisterPeer,
|
||||
}
|
||||
|
||||
impl WorkerInputKind {
|
||||
pub fn is_empty_content_allowed(&self) -> bool {
|
||||
matches!(self, Self::Compact | Self::ListRewindTargets)
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker input request accepted by a Runtime Worker.
|
||||
@@ -15,6 +25,8 @@ pub enum WorkerInputKind {
|
||||
pub struct WorkerInput {
|
||||
pub kind: WorkerInputKind,
|
||||
pub content: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub segments: Option<Vec<Segment>>,
|
||||
}
|
||||
|
||||
impl WorkerInput {
|
||||
@@ -22,6 +34,7 @@ impl WorkerInput {
|
||||
Self {
|
||||
kind: WorkerInputKind::User,
|
||||
content: content.into(),
|
||||
segments: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +42,7 @@ impl WorkerInput {
|
||||
Self {
|
||||
kind: WorkerInputKind::System,
|
||||
content: content.into(),
|
||||
segments: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,6 +537,28 @@ impl Runtime {
|
||||
})
|
||||
}
|
||||
|
||||
/// Return live completion entries for the Worker composer.
|
||||
pub fn worker_completions(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
kind: protocol::CompletionKind,
|
||||
prefix: &str,
|
||||
) -> Result<Vec<protocol::CompletionEntry>, RuntimeError> {
|
||||
let (backend, handle) = {
|
||||
let state = self.lock()?;
|
||||
state.ensure_worker_ref(worker_ref)?;
|
||||
let worker = state.worker(worker_ref)?;
|
||||
(
|
||||
state.execution_backend.clone(),
|
||||
worker.execution_handle.clone(),
|
||||
)
|
||||
};
|
||||
let Some((backend, handle)) = backend.zip(handle) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
Ok(backend.worker_completions(&handle, kind, prefix))
|
||||
}
|
||||
|
||||
fn commit_created_worker(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
@@ -1730,11 +1752,20 @@ fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), R
|
||||
}
|
||||
|
||||
fn validate_worker_input(input: &WorkerInput) -> Result<(), RuntimeError> {
|
||||
if input.content.trim().is_empty() {
|
||||
if !input.kind.is_empty_content_allowed() && input.content.trim().is_empty() {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"worker input content must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if input
|
||||
.segments
|
||||
.as_ref()
|
||||
.is_some_and(|segments| segments.is_empty())
|
||||
{
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"worker input segments must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1742,9 +1773,11 @@ fn validate_worker_input(input: &WorkerInput) -> Result<(), RuntimeError> {
|
||||
fn input_protocol_event(input: &WorkerInput) -> protocol::Event {
|
||||
match input.kind {
|
||||
WorkerInputKind::User => protocol::Event::UserMessage {
|
||||
segments: vec![protocol::Segment::Text {
|
||||
content: input.content.clone(),
|
||||
}],
|
||||
segments: input.segments.clone().unwrap_or_else(|| {
|
||||
vec![protocol::Segment::Text {
|
||||
content: input.content.clone(),
|
||||
}]
|
||||
}),
|
||||
},
|
||||
WorkerInputKind::System => protocol::Event::SystemItem {
|
||||
item: serde_json::json!({
|
||||
@@ -1752,6 +1785,15 @@ fn input_protocol_event(input: &WorkerInput) -> protocol::Event {
|
||||
"content": input.content.clone(),
|
||||
}),
|
||||
},
|
||||
WorkerInputKind::Compact
|
||||
| WorkerInputKind::ListRewindTargets
|
||||
| WorkerInputKind::RegisterPeer => protocol::Event::SystemItem {
|
||||
item: serde_json::json!({
|
||||
"kind": "embedded_worker_command_input",
|
||||
"command": input.kind,
|
||||
"content": input.content.clone(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -996,31 +996,38 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
let WorkerInputKind::User = input.kind else {
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
return WorkerExecutionResult::unsupported(
|
||||
WorkerExecutionOperation::Input,
|
||||
"runtime adapter currently dispatches user input only",
|
||||
);
|
||||
let method = match input.kind {
|
||||
WorkerInputKind::User => Method::Run {
|
||||
input: input
|
||||
.segments
|
||||
.unwrap_or_else(|| vec![Segment::text(input.content.trim().to_string())]),
|
||||
},
|
||||
WorkerInputKind::System => Method::Notify {
|
||||
message: input.content,
|
||||
auto_run: true,
|
||||
},
|
||||
WorkerInputKind::Compact => Method::Compact,
|
||||
WorkerInputKind::ListRewindTargets => Method::ListRewindTargets,
|
||||
WorkerInputKind::RegisterPeer => Method::RegisterPeer {
|
||||
name: input.content.trim().to_string(),
|
||||
},
|
||||
};
|
||||
let content = input.content.trim().to_string();
|
||||
if content.is_empty() {
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
return WorkerExecutionResult::rejected(
|
||||
WorkerExecutionOperation::Input,
|
||||
"runtime adapter rejects empty user input",
|
||||
);
|
||||
}
|
||||
let accepted_run_state = match method {
|
||||
Method::Run { .. } | Method::Notify { .. } | Method::Compact => {
|
||||
WorkerExecutionRunState::Busy
|
||||
}
|
||||
_ => WorkerExecutionRunState::Idle,
|
||||
};
|
||||
let accepted_is_idle = accepted_run_state == WorkerExecutionRunState::Idle;
|
||||
|
||||
let result = self.send_method(
|
||||
WorkerExecutionOperation::Input,
|
||||
worker,
|
||||
Method::Run {
|
||||
input: vec![Segment::text(content)],
|
||||
},
|
||||
WorkerExecutionRunState::Busy,
|
||||
method,
|
||||
accepted_run_state,
|
||||
);
|
||||
if result.outcome != crate::execution::WorkerExecutionOutcome::Accepted {
|
||||
if accepted_is_idle || result.outcome != crate::execution::WorkerExecutionOutcome::Accepted
|
||||
{
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
result
|
||||
@@ -1093,6 +1100,24 @@ where
|
||||
.get(handle.worker_ref())
|
||||
.map(|execution| execution.handle.snapshot_event())
|
||||
}
|
||||
|
||||
fn worker_completions(
|
||||
&self,
|
||||
handle: &WorkerExecutionHandle,
|
||||
kind: protocol::CompletionKind,
|
||||
prefix: &str,
|
||||
) -> Vec<protocol::CompletionEntry> {
|
||||
if handle.backend_id() != self.backend_id() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Ok(workers) = self.workers.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
workers
|
||||
.get(handle.worker_ref())
|
||||
.map(|execution| execution.handle.completion_entries(kind, prefix))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user