feat: add in-process standalone worker host

This commit is contained in:
2026-08-30 11:29:59 +09:00
parent 22867faa9c
commit 4d9b211d69
14 changed files with 782 additions and 6 deletions
+157
View File
@@ -0,0 +1,157 @@
use std::path::PathBuf;
use agen::llm_client::client::LlmClient;
use session_store::{Store, WorkerMetadataStore};
use thiserror::Error;
use workdir::WorkdirSessionHandle;
use crate::PromptCatalogSource;
use crate::controller::{
ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle,
};
use crate::worker::{Worker, WorkerError, WorkerFilesystemAuthority, WorkerWorkspaceContext};
use manifest::WorkerManifest;
/// Filesystem layout used by a Worker controller started through the reusable
/// bootstrap boundary.
#[derive(Debug, Clone)]
pub enum WorkerBootstrapLayout {
/// A direct Worker rooted below the supplied runtime base directory.
Direct { runtime_base: PathBuf },
/// A runtime-managed Worker with an exact persisted run directory.
RuntimeManagedRun { run_dir: PathBuf },
}
/// Construction and controller inputs that are stable for one Worker launch.
pub struct WorkerBootstrap<St> {
manifest: WorkerManifest,
store: St,
prompt_catalog: PromptCatalogSource,
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
layout: WorkerBootstrapLayout,
transport: WorkerControllerTransport,
model_client: Option<Box<dyn LlmClient>>,
workdir_session: Option<WorkdirSessionHandle>,
}
/// Live controller returned only after Worker construction and feature
/// installation have completed successfully.
pub struct BootstrappedWorker {
pub handle: WorkerHandle,
pub shutdown: ShutdownReceiver,
}
#[derive(Debug, Error)]
pub enum WorkerBootstrapError {
#[error("worker construction failed")]
Worker(#[source] WorkerError),
#[error("worker controller startup failed")]
Controller {
#[source]
source: std::io::Error,
cleanup_failed: bool,
},
}
impl<St> WorkerBootstrap<St>
where
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
#[allow(clippy::too_many_arguments)]
pub fn new(
manifest: WorkerManifest,
store: St,
prompt_catalog: PromptCatalogSource,
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
layout: WorkerBootstrapLayout,
transport: WorkerControllerTransport,
) -> Self {
Self {
manifest,
store,
prompt_catalog,
workspace_context,
filesystem_authority,
layout,
transport,
model_client: None,
workdir_session: None,
}
}
/// Inject a process-owned model client. This is primarily useful for
/// embedded hosts and deterministic tests that must not start an external
/// model server.
pub fn with_model_client<C>(mut self, model_client: C) -> Self
where
C: LlmClient + 'static,
{
self.model_client = Some(Box::new(model_client));
self
}
/// Bind an already materialized Workdir session instead of asking the
/// Worker to derive one from filesystem authority.
pub fn with_workdir_session(mut self, workdir_session: WorkdirSessionHandle) -> Self {
self.workdir_session = Some(workdir_session);
self
}
pub async fn start(self) -> Result<BootstrappedWorker, WorkerBootstrapError> {
let mut worker = Worker::from_manifest_with_context_and_model_client(
self.manifest,
self.store,
self.prompt_catalog,
self.workspace_context,
self.filesystem_authority,
self.model_client,
)
.await
.map_err(WorkerBootstrapError::Worker)?;
if let Some(workdir_session) = self.workdir_session {
worker.bind_workdir_session(Some(workdir_session));
}
start_worker_controller(worker, self.layout, self.transport).await
}
}
/// Start the shared direct/runtime-managed controller lifecycle for an already
/// constructed Worker. Restore paths use this after replaying durable state;
/// fresh hosts normally use [`WorkerBootstrap::start`].
pub async fn start_worker_controller<C, St>(
worker: Worker<C, St>,
layout: WorkerBootstrapLayout,
transport: WorkerControllerTransport,
) -> Result<BootstrappedWorker, WorkerBootstrapError>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
let cleanup_session = worker.workdir_session().cloned();
let controller = match layout {
WorkerBootstrapLayout::Direct { runtime_base } => {
WorkerController::spawn_with_transport(worker, &runtime_base, transport).await
}
WorkerBootstrapLayout::RuntimeManagedRun { run_dir } => {
WorkerController::spawn_runtime_managed_run_with_transport(worker, &run_dir, transport)
.await
}
};
match controller {
Ok((handle, shutdown)) => Ok(BootstrappedWorker { handle, shutdown }),
Err(source) => {
let cleanup_failed = match cleanup_session {
Some(session) => session.close().await.is_err(),
None => false,
};
Err(WorkerBootstrapError::Controller {
source,
cleanup_failed,
})
}
}
}
+14
View File
@@ -240,6 +240,20 @@ impl WorkerController {
.await
}
/// Spawn a direct Worker while letting an in-process host select the
/// controller transport explicitly.
pub async fn spawn_with_transport<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
transport: WorkerControllerTransport,
) -> Result<(WorkerHandle, ShutdownReceiver), std::io::Error>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
Self::spawn_inner(worker, runtime_base, false, None, transport).await
}
/// Spawn a Worker owned by `worker-runtime`.
///
/// The controller still uses an ephemeral directory for Unix sockets and
+13 -4
View File
@@ -3,8 +3,8 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
use crate::{
PromptCatalogSource, Worker, WorkerController, WorkerFilesystemAuthority,
WorkerWorkspaceContext,
PromptCatalogSource, Worker, WorkerBootstrapLayout, WorkerControllerTransport,
WorkerFilesystemAuthority, WorkerWorkspaceContext, start_worker_controller,
};
use clap::{CommandFactory, FromArgMatches, Parser};
use manifest::{Permission, ScopeConfig, ScopeRule, WorkerManifest, WorkerManifestConfig, paths};
@@ -634,13 +634,22 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
let (handle, shutdown_rx) = match WorkerController::spawn(worker, &runtime_base).await {
Ok(pair) => pair,
let started = match start_worker_controller(
worker,
WorkerBootstrapLayout::Direct {
runtime_base: runtime_base.clone(),
},
WorkerControllerTransport::UnixSocket,
)
.await
{
Ok(started) => started,
Err(e) => {
eprintln!("error: failed to start worker controller: {e}");
return ExitCode::FAILURE;
}
};
let (handle, shutdown_rx) = (started.handle, started.shutdown);
let socket_path = handle.runtime_dir.socket_path();
// Machine-readable ready line for parents that spawned this Worker
+5
View File
@@ -1,3 +1,4 @@
pub mod bootstrap;
pub mod compact;
pub mod controller;
pub mod discovery;
@@ -23,6 +24,10 @@ mod interrupt_prep;
mod permission;
mod worker;
pub use bootstrap::{
BootstrappedWorker, WorkerBootstrap, WorkerBootstrapError, WorkerBootstrapLayout,
start_worker_controller,
};
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
pub use controller::{ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle};
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
+47 -2
View File
@@ -5107,15 +5107,35 @@ where
loader: PromptCatalogSource,
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
) -> Result<Self, WorkerError> {
Self::from_manifest_with_context_and_model_client(
manifest,
store,
loader,
workspace_context,
filesystem_authority,
None,
)
.await
}
pub(crate) async fn from_manifest_with_context_and_model_client(
manifest: WorkerManifest,
store: St,
loader: PromptCatalogSource,
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
model_client: Option<Box<dyn LlmClient>>,
) -> Result<Self, WorkerError> {
validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?;
let common = prepare_worker_common_with_context(
let common = prepare_worker_common_with_context_and_model_client(
&manifest,
&loader,
/* parse_template */ true,
workspace_context,
filesystem_authority,
manifest.scope.clone(),
model_client,
)?;
// Segment creation is deferred to the first run (see
@@ -6592,6 +6612,26 @@ fn prepare_worker_common_with_context(
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
scope_config: ScopeConfig,
) -> Result<WorkerCommon, WorkerError> {
prepare_worker_common_with_context_and_model_client(
manifest,
loader,
parse_template,
workspace_context,
filesystem_authority,
scope_config,
None,
)
}
fn prepare_worker_common_with_context_and_model_client(
manifest: &WorkerManifest,
loader: &PromptCatalogSource,
parse_template: bool,
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
scope_config: ScopeConfig,
model_client: Option<Box<dyn LlmClient>>,
) -> Result<WorkerCommon, WorkerError> {
let filesystem_authority = match filesystem_authority {
WorkerFilesystemAuthority::None => WorkerFilesystemAuthority::None,
@@ -6627,6 +6667,7 @@ fn prepare_worker_common_with_context(
workspace_context,
filesystem_authority,
scope,
model_client,
)
}
@@ -6637,6 +6678,7 @@ fn prepare_worker_common_from_scope(
workspace_context: WorkerWorkspaceContext,
filesystem_authority: WorkerFilesystemAuthority,
scope: Scope,
model_client: Option<Box<dyn LlmClient>>,
) -> Result<WorkerCommon, WorkerError> {
if let Some(local) = filesystem_authority.as_local() {
if !scope.is_readable(&local.root) {
@@ -6653,7 +6695,10 @@ fn prepare_worker_common_from_scope(
let delegation_scope =
DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::Scope)?;
let client = crate::model_client::build_client(&manifest.model)?;
let client = match model_client {
Some(client) => client,
None => crate::model_client::build_client(&manifest.model)?,
};
let prompts = Arc::new(ArcSwap::from(PromptCatalog::load(loader)?));
let system_prompt_template = if parse_template {
Some(