feat: add in-process standalone worker host
This commit is contained in:
Generated
+17
@@ -4616,6 +4616,23 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "standalone"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"agen",
|
||||||
|
"async-trait",
|
||||||
|
"futures",
|
||||||
|
"manifest",
|
||||||
|
"protocol",
|
||||||
|
"session-store",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
"worker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/agen",
|
"crates/agen",
|
||||||
"crates/agen-macros",
|
"crates/agen-macros",
|
||||||
"crates/session-store",
|
"crates/session-store",
|
||||||
|
"crates/standalone",
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
"crates/mcp",
|
"crates/mcp",
|
||||||
@@ -36,6 +37,7 @@ default-members = [
|
|||||||
"crates/agen",
|
"crates/agen",
|
||||||
"crates/agen-macros",
|
"crates/agen-macros",
|
||||||
"crates/session-store",
|
"crates/session-store",
|
||||||
|
"crates/standalone",
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
"crates/mcp",
|
"crates/mcp",
|
||||||
@@ -87,6 +89,7 @@ protocol = { path = "crates/protocol" }
|
|||||||
session-metrics = { path = "crates/session-metrics" }
|
session-metrics = { path = "crates/session-metrics" }
|
||||||
session-analytics = { path = "crates/session-analytics" }
|
session-analytics = { path = "crates/session-analytics" }
|
||||||
session-store = { path = "crates/session-store" }
|
session-store = { path = "crates/session-store" }
|
||||||
|
standalone = { path = "crates/standalone" }
|
||||||
secrets = { path = "crates/secrets" }
|
secrets = { path = "crates/secrets" }
|
||||||
tools = { path = "crates/tools" }
|
tools = { path = "crates/tools" }
|
||||||
config-source = { path = "crates/config-source" }
|
config-source = { path = "crates/config-source" }
|
||||||
|
|||||||
@@ -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"] }
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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};
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
.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`.
|
/// Spawn a Worker owned by `worker-runtime`.
|
||||||
///
|
///
|
||||||
/// The controller still uses an ephemeral directory for Unix sockets and
|
/// 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 std::process::ExitCode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PromptCatalogSource, Worker, WorkerController, WorkerFilesystemAuthority,
|
PromptCatalogSource, Worker, WorkerBootstrapLayout, WorkerControllerTransport,
|
||||||
WorkerWorkspaceContext,
|
WorkerFilesystemAuthority, WorkerWorkspaceContext, start_worker_controller,
|
||||||
};
|
};
|
||||||
use clap::{CommandFactory, FromArgMatches, Parser};
|
use clap::{CommandFactory, FromArgMatches, Parser};
|
||||||
use manifest::{Permission, ScopeConfig, ScopeRule, WorkerManifest, WorkerManifestConfig, paths};
|
use manifest::{Permission, ScopeConfig, ScopeRule, WorkerManifest, WorkerManifestConfig, paths};
|
||||||
@@ -634,13 +634,22 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
|||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (handle, shutdown_rx) = match WorkerController::spawn(worker, &runtime_base).await {
|
let started = match start_worker_controller(
|
||||||
Ok(pair) => pair,
|
worker,
|
||||||
|
WorkerBootstrapLayout::Direct {
|
||||||
|
runtime_base: runtime_base.clone(),
|
||||||
|
},
|
||||||
|
WorkerControllerTransport::UnixSocket,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(started) => started,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to start worker controller: {e}");
|
eprintln!("error: failed to start worker controller: {e}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let (handle, shutdown_rx) = (started.handle, started.shutdown);
|
||||||
|
|
||||||
let socket_path = handle.runtime_dir.socket_path();
|
let socket_path = handle.runtime_dir.socket_path();
|
||||||
// Machine-readable ready line for parents that spawned this Worker
|
// Machine-readable ready line for parents that spawned this Worker
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod bootstrap;
|
||||||
pub mod compact;
|
pub mod compact;
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
@@ -23,6 +24,10 @@ mod interrupt_prep;
|
|||||||
mod permission;
|
mod permission;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
|
pub use bootstrap::{
|
||||||
|
BootstrappedWorker, WorkerBootstrap, WorkerBootstrapError, WorkerBootstrapLayout,
|
||||||
|
start_worker_controller,
|
||||||
|
};
|
||||||
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
|
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
|
||||||
pub use controller::{ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle};
|
pub use controller::{ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle};
|
||||||
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
|
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
|
||||||
|
|||||||
@@ -5107,15 +5107,35 @@ where
|
|||||||
loader: PromptCatalogSource,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
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> {
|
) -> Result<Self, WorkerError> {
|
||||||
validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?;
|
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,
|
&manifest,
|
||||||
&loader,
|
&loader,
|
||||||
/* parse_template */ true,
|
/* parse_template */ true,
|
||||||
workspace_context,
|
workspace_context,
|
||||||
filesystem_authority,
|
filesystem_authority,
|
||||||
manifest.scope.clone(),
|
manifest.scope.clone(),
|
||||||
|
model_client,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Segment creation is deferred to the first run (see
|
// Segment creation is deferred to the first run (see
|
||||||
@@ -6592,6 +6612,26 @@ fn prepare_worker_common_with_context(
|
|||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
scope_config: ScopeConfig,
|
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> {
|
) -> Result<WorkerCommon, WorkerError> {
|
||||||
let filesystem_authority = match filesystem_authority {
|
let filesystem_authority = match filesystem_authority {
|
||||||
WorkerFilesystemAuthority::None => WorkerFilesystemAuthority::None,
|
WorkerFilesystemAuthority::None => WorkerFilesystemAuthority::None,
|
||||||
@@ -6627,6 +6667,7 @@ fn prepare_worker_common_with_context(
|
|||||||
workspace_context,
|
workspace_context,
|
||||||
filesystem_authority,
|
filesystem_authority,
|
||||||
scope,
|
scope,
|
||||||
|
model_client,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6637,6 +6678,7 @@ fn prepare_worker_common_from_scope(
|
|||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
|
model_client: Option<Box<dyn LlmClient>>,
|
||||||
) -> Result<WorkerCommon, WorkerError> {
|
) -> Result<WorkerCommon, WorkerError> {
|
||||||
if let Some(local) = filesystem_authority.as_local() {
|
if let Some(local) = filesystem_authority.as_local() {
|
||||||
if !scope.is_readable(&local.root) {
|
if !scope.is_readable(&local.root) {
|
||||||
@@ -6653,7 +6695,10 @@ fn prepare_worker_common_from_scope(
|
|||||||
let delegation_scope =
|
let delegation_scope =
|
||||||
DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::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 prompts = Arc::new(ArcSwap::from(PromptCatalog::load(loader)?));
|
||||||
let system_prompt_template = if parse_template {
|
let system_prompt_template = if parse_template {
|
||||||
Some(
|
Some(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ It is not a dumping ground for external research, old plans, API inventories, or
|
|||||||
15. [`development/rust-testing-strategy.md`](development/rust-testing-strategy.md) — what Yoi Rust tests should prove, where they belong, and how to name them.
|
15. [`development/rust-testing-strategy.md`](development/rust-testing-strategy.md) — what Yoi Rust tests should prove, where they belong, and how to name them.
|
||||||
16. [`development/validation.md`](development/validation.md) — how to check changes.
|
16. [`development/validation.md`](development/validation.md) — how to check changes.
|
||||||
17. [`development/workspace-schema-migrations.md`](development/workspace-schema-migrations.md) — how to preflight, apply, verify, and roll back control-plane SQLite schema changes.
|
17. [`development/workspace-schema-migrations.md`](development/workspace-schema-migrations.md) — how to preflight, apply, verify, and roll back control-plane SQLite schema changes.
|
||||||
|
18. [`design/standalone-agent-host.md`](design/standalone-agent-host.md) — in-process standalone Worker host の依存方向、authority、lifecycle、非目標。
|
||||||
|
|
||||||
## What belongs here
|
## What belongs here
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Standalone Agent Host
|
||||||
|
|
||||||
|
`crates/standalone` は、既存の Yoi Worker を同一プロセス内で起動する最小の host 境界である。
|
||||||
|
新しい Agent 実行系ではなく、`manifest`、`worker`、`session-store`、`workdir` の既存契約を固定構成で組み立てる。
|
||||||
|
|
||||||
|
## 依存方向
|
||||||
|
|
||||||
|
```text
|
||||||
|
standalone
|
||||||
|
├─ manifest
|
||||||
|
├─ worker
|
||||||
|
├─ session-store
|
||||||
|
└─ protocol
|
||||||
|
|
||||||
|
worker
|
||||||
|
└─ WorkerBootstrap / start_worker_controller
|
||||||
|
```
|
||||||
|
|
||||||
|
`standalone` は `tui`、`worker-runtime`、`yoi-workspace-server` に依存しない。
|
||||||
|
`worker` の direct entrypoint と `standalone` は同じ `start_worker_controller` lifecycle を使い、後者は `WorkerBootstrap` で fresh Worker construction も共有する。
|
||||||
|
|
||||||
|
## authority と lifecycle
|
||||||
|
|
||||||
|
- launch は `ProfileExecutionTarget::Standalone` で built-in/XDG Profile を resolve する。repository-local Profile と path selector は authority にしない。
|
||||||
|
- canonical cwd を `WorkerFilesystemAuthority::local` の root/cwd とし、host ごとに top-level Worker と process-owned `WorkdirSession` を一つ作る。
|
||||||
|
- Controller transport は `InProcess` に固定する。通常起動で Worker subprocess、HTTP/WS server、Unix socket を作らない。
|
||||||
|
- model provider は通常の resolved Manifest から構築する。埋め込み host と deterministic test は `start_with_model_client` で同じ bootstrap に process-owned client を注入できる。
|
||||||
|
- feature plan/install は既存 `WorkerController` が行う。Task や optional direct SubWorker を standalone 側で再実装しない。
|
||||||
|
- `shutdown()` は既存 `Method::Shutdown` を送り、controller が active run、SubWorker registry、Workdir session、MachineScope allocation を順に片付けた後の confirmation を待つ。
|
||||||
|
- startup error は category のみを公開し、credential、prompt 本文、session metadata、内部 path を error text に含めない。
|
||||||
|
|
||||||
|
## 非目標
|
||||||
|
|
||||||
|
- TUI/CLI routing や画面実装
|
||||||
|
- Runtime / Workspace Server / Orchestrator / Ticket authority の内包
|
||||||
|
- standalone 独自の HTTP/WS API
|
||||||
|
- Worker/Tool/Task/SubWorker protocol の fork
|
||||||
|
- subprocess Worker launcher や runtime-owned Worker catalog
|
||||||
Reference in New Issue
Block a user