feat: add in-process standalone worker host
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user