worker: run sub-workers as internal sessions

This commit is contained in:
2026-08-07 01:44:04 +09:00
parent 485918ebe3
commit c79db24016
13 changed files with 531 additions and 2260 deletions
+80 -17
View File
@@ -228,6 +228,7 @@ impl InternalWorkerSessionStatus {
#[derive(Debug, thiserror::Error)]
pub(crate) enum InternalWorkerSessionError {
#[cfg(test)]
#[error("failed to build internal Worker session: {message}")]
Build { message: String },
#[error("internal Worker session is busy")]
@@ -254,7 +255,6 @@ pub(crate) struct InternalWorkerSessionHandle {
store: EphemeralSessionStore,
session_id: SessionId,
segment_id: SegmentId,
last_error: Arc<Mutex<Option<String>>>,
state_changed: Arc<tokio::sync::Notify>,
}
@@ -269,10 +269,6 @@ impl InternalWorkerSessionHandle {
.unwrap_or_default()
}
pub(crate) fn last_error(&self) -> Option<String> {
self.last_error.lock().ok().and_then(|error| error.clone())
}
pub(crate) async fn send(
&self,
input: impl Into<String>,
@@ -310,6 +306,7 @@ impl InternalWorkerSessionHandle {
Ok(())
}
#[cfg(test)]
pub(crate) async fn wait_until_idle(&self) -> InternalWorkerSessionStatus {
loop {
let notified = self.state_changed.notified();
@@ -346,6 +343,7 @@ impl InternalWorkerSessionHandle {
}
/// Start a reusable Internal Worker session and accept its first turn.
#[cfg(test)]
pub(crate) async fn spawn_internal_worker_session(
spec: InternalWorkerSpec,
) -> Result<InternalWorkerSessionHandle, InternalWorkerSessionError> {
@@ -418,13 +416,21 @@ pub(crate) async fn spawn_internal_worker_session(
});
}
spawn_prepared_internal_worker_session(worker, store, input, None).await
}
pub(crate) async fn spawn_prepared_internal_worker_session(
mut worker: Worker<Box<dyn LlmClient>, EphemeralSessionStore>,
store: EphemeralSessionStore,
input: String,
on_turn_end: Option<Arc<dyn Fn(InternalWorkerSessionStatus) + Send + Sync>>,
) -> Result<InternalWorkerSessionHandle, InternalWorkerSessionError> {
let session_id = worker.session_id();
let segment_id = worker.segment_id();
let (command_tx, mut command_rx) = tokio::sync::mpsc::channel(8);
let status = Arc::new(std::sync::atomic::AtomicU8::new(
InternalWorkerSessionStatus::Idle.encode(),
));
let last_error = Arc::new(Mutex::new(None));
let state_changed = Arc::new(tokio::sync::Notify::new());
let handle = InternalWorkerSessionHandle {
command_tx,
@@ -432,7 +438,6 @@ pub(crate) async fn spawn_internal_worker_session(
store,
session_id,
segment_id,
last_error: last_error.clone(),
state_changed: state_changed.clone(),
};
@@ -445,14 +450,13 @@ pub(crate) async fn spawn_internal_worker_session(
loop {
tokio::select! {
result = &mut run => {
match result {
Ok(_) => {
status.store(InternalWorkerSessionStatus::Idle.encode(), std::sync::atomic::Ordering::Release);
}
Err(error) => {
*last_error.lock().expect("internal Worker session error lock") = Some(error.to_string());
status.store(InternalWorkerSessionStatus::Failed.encode(), std::sync::atomic::Ordering::Release);
}
let turn_status = match result {
Ok(_) => InternalWorkerSessionStatus::Idle,
Err(_) => InternalWorkerSessionStatus::Failed,
};
status.store(turn_status.encode(), std::sync::atomic::Ordering::Release);
if let Some(callback) = &on_turn_end {
callback(turn_status);
}
state_changed.notify_waiters();
break;
@@ -501,9 +505,10 @@ pub(crate) async fn spawn_internal_worker_session(
/// Keeping the normal Store contract makes history/lifecycle/error records identical to a normal
/// Worker while avoiding a second public persistence/catalog policy for helper executions.
#[derive(Clone, Default)]
struct EphemeralSessionStore {
pub(crate) struct EphemeralSessionStore {
entries: Arc<Mutex<HashMap<(SessionId, SegmentId), Vec<LogEntry>>>>,
traces: Arc<Mutex<HashMap<(SessionId, SegmentId), Vec<TraceEntry>>>>,
worker_metadata: Arc<Mutex<HashMap<String, session_store::WorkerMetadata>>>,
}
impl EphemeralSessionStore {
@@ -631,6 +636,65 @@ impl Store for EphemeralSessionStore {
}
}
impl session_store::WorkerMetadataStore for EphemeralSessionStore {
fn write(
&self,
metadata: &session_store::WorkerMetadata,
) -> Result<(), session_store::WorkerStoreError> {
self.worker_metadata
.lock()
.map_err(|_| {
session_store::WorkerStoreError::Io(std::io::Error::other(
"ephemeral metadata lock poisoned",
))
})?
.insert(metadata.worker_name.clone(), metadata.clone());
Ok(())
}
fn read_by_name(
&self,
worker_name: &str,
) -> Result<Option<session_store::WorkerMetadata>, session_store::WorkerStoreError> {
Ok(self
.worker_metadata
.lock()
.map_err(|_| {
session_store::WorkerStoreError::Io(std::io::Error::other(
"ephemeral metadata lock poisoned",
))
})?
.get(worker_name)
.cloned())
}
fn list_names(&self) -> Result<Vec<String>, session_store::WorkerStoreError> {
Ok(self
.worker_metadata
.lock()
.map_err(|_| {
session_store::WorkerStoreError::Io(std::io::Error::other(
"ephemeral metadata lock poisoned",
))
})?
.keys()
.cloned()
.collect())
}
fn delete_by_name(&self, worker_name: &str) -> Result<(), session_store::WorkerStoreError> {
self.worker_metadata
.lock()
.map_err(|_| {
session_store::WorkerStoreError::Io(std::io::Error::other(
"ephemeral metadata lock poisoned",
))
})?
.remove(worker_name);
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::pin::Pin;
@@ -802,7 +866,6 @@ permission = "write"
);
assert_eq!(calls.load(Ordering::SeqCst), 2);
assert!(handle.entries().len() > entries_after_first);
assert!(handle.last_error().is_none());
handle.stop().await.expect("stop Internal Worker session");
assert_eq!(handle.status(), InternalWorkerSessionStatus::Stopped);