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
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "standalone"
description = "In-process standalone Worker host"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]
agen.workspace = true
manifest.workspace = true
protocol.workspace = true
session-store.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "sync", "time"] }
worker.workspace = true
[dev-dependencies]
async-trait.workspace = true
futures.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
uuid = { workspace = true, features = ["v7"] }
+154
View File
@@ -0,0 +1,154 @@
use std::time::Duration;
use agen::llm_client::client::LlmClient;
use protocol::{Event, Method};
use session_store::{CombinedStore, FsStore, FsWorkerStore};
use thiserror::Error;
use tokio::sync::broadcast;
use worker::bootstrap::{WorkerBootstrap, WorkerBootstrapError, WorkerBootstrapLayout};
use worker::controller::WorkerControllerTransport;
use worker::{WorkerError, WorkerFilesystemAuthority, WorkerWorkspaceContext};
use crate::launch::ResolvedStandaloneLaunch;
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
/// One top-level Worker plus its process-owned local Workdir session.
///
/// The host deliberately exposes the existing typed Worker protocol rather
/// than owning an HTTP/WebSocket server or a second execution model.
pub struct StandaloneHost {
handle: worker::WorkerHandle,
shutdown: worker::controller::ShutdownReceiver,
shutdown_timeout: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum StandaloneStartupError {
#[error("the standalone state store could not be opened")]
StateStore,
#[error("the resolved Worker configuration is invalid")]
WorkerConfiguration,
#[error("the configured model provider is unavailable")]
ModelProvider,
#[error("the fixed standalone feature composition could not be installed")]
FeatureComposition,
#[error("the in-process Worker controller could not start")]
Controller,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum StandaloneRequestError {
#[error("the standalone Worker is no longer accepting requests")]
WorkerUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum StandaloneShutdownError {
#[error("the standalone Worker did not stop before the shutdown deadline")]
DeadlineExceeded,
#[error("the standalone Worker shutdown confirmation was lost")]
ConfirmationLost,
}
impl StandaloneHost {
pub async fn start(launch: ResolvedStandaloneLaunch) -> Result<Self, StandaloneStartupError> {
Self::start_with_optional_model_client(launch, None).await
}
pub async fn start_with_model_client<C>(
launch: ResolvedStandaloneLaunch,
model_client: C,
) -> Result<Self, StandaloneStartupError>
where
C: LlmClient + 'static,
{
Self::start_with_optional_model_client(launch, Some(Box::new(model_client))).await
}
async fn start_with_optional_model_client(
launch: ResolvedStandaloneLaunch,
model_client: Option<Box<dyn LlmClient>>,
) -> Result<Self, StandaloneStartupError> {
std::fs::create_dir_all(&launch.state_dir)
.map_err(|_| StandaloneStartupError::StateStore)?;
let session_store = FsStore::new(launch.state_dir.join("sessions"))
.map_err(|_| StandaloneStartupError::StateStore)?;
let worker_store = FsWorkerStore::new(launch.state_dir.join("workers"))
.map_err(|_| StandaloneStartupError::StateStore)?;
let store = CombinedStore::new(session_store, worker_store);
let filesystem_authority =
WorkerFilesystemAuthority::local(launch.cwd.clone(), launch.cwd.clone());
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
let runtime_base = launch.state_dir.join("runtime");
let mut bootstrap = WorkerBootstrap::new(
launch.profile.manifest,
store,
launch.prompt_catalog,
workspace_context,
filesystem_authority,
WorkerBootstrapLayout::Direct { runtime_base },
WorkerControllerTransport::InProcess,
);
if let Some(model_client) = model_client {
bootstrap = bootstrap.with_model_client(model_client);
}
let started = bootstrap.start().await.map_err(classify_startup_error)?;
Ok(Self {
handle: started.handle,
shutdown: started.shutdown,
shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
})
}
pub async fn send(&self, method: Method) -> Result<(), StandaloneRequestError> {
self.handle
.send(method)
.await
.map_err(|_| StandaloneRequestError::WorkerUnavailable)
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.handle.subscribe()
}
pub fn snapshot(&self) -> Event {
self.handle.snapshot_event()
}
pub fn with_shutdown_timeout(mut self, shutdown_timeout: Duration) -> Self {
self.shutdown_timeout = shutdown_timeout;
self
}
pub async fn shutdown(self) -> Result<(), StandaloneShutdownError> {
let StandaloneHost {
handle,
shutdown,
shutdown_timeout,
} = self;
let _ = handle.send(Method::Shutdown).await;
match tokio::time::timeout(shutdown_timeout, shutdown).await {
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => Err(StandaloneShutdownError::ConfirmationLost),
Err(_) => Err(StandaloneShutdownError::DeadlineExceeded),
}
}
}
fn classify_startup_error(error: WorkerBootstrapError) -> StandaloneStartupError {
match error {
WorkerBootstrapError::Worker(WorkerError::Provider(_)) => {
StandaloneStartupError::ModelProvider
}
WorkerBootstrapError::Worker(_) => StandaloneStartupError::WorkerConfiguration,
WorkerBootstrapError::Controller { source, .. }
if source.kind() == std::io::ErrorKind::Other =>
{
StandaloneStartupError::FeatureComposition
}
WorkerBootstrapError::Controller { .. } => StandaloneStartupError::Controller,
}
}
+86
View File
@@ -0,0 +1,86 @@
use std::path::{Path, PathBuf};
use manifest::{
ProfileExecutionTarget, ProfileResolveOptions, ProfileResolver, ProfileSelector,
ResolvedProfile,
};
use thiserror::Error;
use worker::PromptCatalogSource;
/// Process launch input resolved before any Worker/session side effect occurs.
#[derive(Debug, Clone)]
pub struct StandaloneLaunchConfig {
pub cwd: PathBuf,
pub state_dir: PathBuf,
pub profile: ProfileSelector,
pub worker_name: String,
}
pub struct ResolvedStandaloneLaunch {
pub cwd: PathBuf,
pub state_dir: PathBuf,
pub profile: ResolvedProfile,
pub prompt_catalog: PromptCatalogSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum StandaloneLaunchError {
#[error("the standalone working directory is unavailable")]
WorkingDirectoryUnavailable,
#[error("path-based profiles are not standalone launch authority")]
PathProfileUnsupported,
#[error("the standalone profile could not be resolved")]
ProfileResolutionFailed,
}
impl StandaloneLaunchConfig {
pub fn new(
cwd: impl Into<PathBuf>,
state_dir: impl Into<PathBuf>,
profile: ProfileSelector,
worker_name: impl Into<String>,
) -> Self {
Self {
cwd: cwd.into(),
state_dir: state_dir.into(),
profile,
worker_name: worker_name.into(),
}
}
/// Resolve only built-in/XDG profile authority and bind standalone scope
/// to the canonical process cwd. Repository-local profile discovery is
/// deliberately not part of this path.
pub fn resolve(self) -> Result<ResolvedStandaloneLaunch, StandaloneLaunchError> {
if matches!(self.profile, ProfileSelector::Path { .. }) {
return Err(StandaloneLaunchError::PathProfileUnsupported);
}
let cwd = canonical_directory(&self.cwd)?;
let profile = ProfileResolver::new()
.with_workspace_base(&cwd)
.resolve_for_target(
&self.profile,
ProfileResolveOptions {
worker_name: Some(self.worker_name),
},
ProfileExecutionTarget::Standalone,
)
.map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?;
Ok(ResolvedStandaloneLaunch {
cwd,
state_dir: self.state_dir,
profile,
prompt_catalog: PromptCatalogSource::builtins_only(),
})
}
}
fn canonical_directory(path: &Path) -> Result<PathBuf, StandaloneLaunchError> {
let path = std::fs::canonicalize(path)
.map_err(|_| StandaloneLaunchError::WorkingDirectoryUnavailable)?;
if !path.is_dir() {
return Err(StandaloneLaunchError::WorkingDirectoryUnavailable);
}
Ok(path)
}
+13
View File
@@ -0,0 +1,13 @@
//! In-process standalone host for one top-level Yoi Worker.
//!
//! The crate composes existing `worker`, `manifest`, `session-store`, and
//! `workdir` contracts. It intentionally owns no TUI, Runtime, Workspace
//! Server, HTTP, WebSocket, subprocess Worker, or alternative execution path.
pub mod host;
pub mod launch;
pub use host::{
StandaloneHost, StandaloneRequestError, StandaloneShutdownError, StandaloneStartupError,
};
pub use launch::{ResolvedStandaloneLaunch, StandaloneLaunchConfig, StandaloneLaunchError};
+212
View File
@@ -0,0 +1,212 @@
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use agen::llm_client::client::LlmClient;
use agen::llm_client::error::ClientError;
use agen::llm_client::event::{Event as LlmEvent, StopReason};
use agen::llm_client::types::Request;
use async_trait::async_trait;
use futures::{Stream, stream};
use protocol::{Event, Method};
use standalone::{StandaloneHost, StandaloneLaunchConfig};
use uuid::Uuid;
#[derive(Clone)]
struct ScriptedClient {
responses: Arc<Mutex<VecDeque<Vec<LlmEvent>>>>,
requests: Arc<Mutex<Vec<Request>>>,
}
impl ScriptedClient {
fn new(responses: Vec<Vec<LlmEvent>>) -> Self {
Self {
responses: Arc::new(Mutex::new(responses.into())),
requests: Arc::new(Mutex::new(Vec::new())),
}
}
fn requests(&self) -> Vec<Request> {
self.requests.lock().expect("requests lock").clone()
}
}
#[async_trait]
impl LlmClient for ScriptedClient {
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<LlmEvent, ClientError>> + Send>>, ClientError>
{
self.requests.lock().expect("requests lock").push(request);
let response = self
.responses
.lock()
.expect("responses lock")
.pop_front()
.expect("scripted response");
Ok(Box::pin(stream::iter(response.into_iter().map(Ok))))
}
fn clone_boxed(&self) -> Box<dyn LlmClient> {
Box::new(self.clone())
}
}
#[tokio::test]
async fn in_process_host_runs_text_and_read_tool_then_shuts_down() {
let temp = tempfile::tempdir().expect("tempdir");
std::fs::write(temp.path().join("probe.txt"), "standalone tool evidence\n")
.expect("write probe");
let worker_name = format!("standalone-{}", Uuid::now_v7());
let launch = StandaloneLaunchConfig::new(
temp.path(),
temp.path().join("state"),
manifest::ProfileSelector::Default,
&worker_name,
)
.resolve()
.expect("resolve standalone profile");
let client = ScriptedClient::new(vec![
vec![
LlmEvent::tool_use_start(0, "read-1", "Read"),
LlmEvent::tool_input_delta(0, r#"{"file_path":"probe.txt"}"#),
LlmEvent::tool_use_stop(0),
],
vec![
LlmEvent::text_block_start(0),
LlmEvent::text_delta(0, "standalone response"),
LlmEvent::text_block_stop(0, Some(StopReason::EndTurn)),
],
]);
let inspection = client.clone();
let host = StandaloneHost::start_with_model_client(launch, client)
.await
.expect("start in-process host");
let mut events = host.subscribe();
host.send(Method::run_text("read the probe"))
.await
.expect("submit input");
tokio::time::timeout(Duration::from_secs(30), async {
let mut saw_text = false;
let mut saw_tool_result = false;
loop {
match events.recv().await.expect("worker event") {
Event::TextDelta { text } if text.contains("standalone response") => {
saw_text = true;
}
Event::ToolResult { .. } => {
saw_tool_result = true;
}
Event::RunEnd { .. } => {
assert!(saw_text, "stream must expose the model text delta");
assert!(saw_tool_result, "stream must expose the tool result");
break;
}
_ => {}
}
}
})
.await
.expect("run completed");
let requests = inspection.requests();
assert_eq!(requests.len(), 2);
let tool_names = requests[0]
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
assert!(tool_names.contains(&"Read"));
assert!(tool_names.contains(&"TaskCreate"));
assert!(tool_names.contains(&"SubWorkerSpawn"));
assert!(format!("{:?}", requests[1].items).contains("standalone tool evidence"));
assert!(
!temp
.path()
.join("state/runtime")
.join(&worker_name)
.join("worker.sock")
.exists()
);
host.shutdown().await.expect("graceful shutdown");
}
#[tokio::test]
async fn state_store_failure_is_redacted_and_starts_no_controller() {
let temp = tempfile::tempdir().expect("tempdir");
let state_path = temp.path().join("state-file-with-secret-name");
std::fs::write(&state_path, "not a directory").expect("write blocking file");
let launch = StandaloneLaunchConfig::new(
temp.path(),
&state_path,
manifest::ProfileSelector::Default,
format!("standalone-failure-{}", Uuid::now_v7()),
)
.resolve()
.expect("resolve launch");
let client = ScriptedClient::new(Vec::new());
let error = StandaloneHost::start_with_model_client(launch, client)
.await
.err()
.expect("state store startup rejected");
assert_eq!(error, standalone::StandaloneStartupError::StateStore);
assert_eq!(
error.to_string(),
"the standalone state store could not be opened"
);
assert!(!error.to_string().contains("secret-name"));
assert!(
!temp
.path()
.join("state-file-with-secret-name/runtime")
.exists()
);
}
#[test]
fn standalone_crate_has_no_tui_runtime_or_workspace_server_dependency() {
let manifest = include_str!("../Cargo.toml");
let dependencies = manifest
.split("[dependencies]")
.nth(1)
.expect("dependencies section")
.split("[dev-dependencies]")
.next()
.expect("dependency body");
for forbidden in ["tui", "worker-runtime", "yoi-workspace-server"] {
assert!(
!dependencies.lines().any(|line| {
line.split_once('=')
.is_some_and(|(name, _)| name.trim() == forbidden)
}),
"standalone must not depend on {forbidden}"
);
}
}
#[test]
fn launch_rejects_path_profile_before_worker_startup() {
let temp = tempfile::tempdir().expect("tempdir");
let error = StandaloneLaunchConfig::new(
temp.path(),
temp.path().join("state"),
manifest::ProfileSelector::Path {
path: temp.path().join("profile.dcdl"),
},
"standalone-path-profile",
)
.resolve()
.err()
.expect("path profile rejected");
assert_eq!(
error,
standalone::StandaloneLaunchError::PathProfileUnsupported
);
}
+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(