chore: merge standalone feature into companion integration
# Conflicts: # crates/client/src/target.rs # crates/client/src/ticket_role.rs # crates/manifest/src/profile.rs # crates/tui/src/dashboard/tests.rs # crates/tui/src/worker_list.rs # crates/workspace-server/src/hosts.rs # crates/yoi/src/main.rs
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
/// A constructed Worker whose host-owned live bindings can still be installed
|
||||
/// before Feature installation and controller exposure.
|
||||
pub struct PreparedWorker<C: LlmClient, St: Store> {
|
||||
worker: Worker<C, St>,
|
||||
layout: WorkerBootstrapLayout,
|
||||
transport: WorkerControllerTransport,
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Construct the Worker without exposing a controller handle. Runtime hosts
|
||||
/// use this seam to bind Workdir, observation, Flow, and other live services
|
||||
/// before [`PreparedWorker::start`] performs Feature installation.
|
||||
pub async fn prepare(
|
||||
self,
|
||||
) -> Result<PreparedWorker<Box<dyn LlmClient>, St>, 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));
|
||||
}
|
||||
Ok(PreparedWorker::new(worker, self.layout, self.transport))
|
||||
}
|
||||
|
||||
pub async fn prepare_restored(
|
||||
self,
|
||||
worker_name: &str,
|
||||
) -> Result<PreparedWorker<Box<dyn LlmClient>, St>, WorkerBootstrapError> {
|
||||
let mut worker =
|
||||
Worker::restore_pending_from_worker_metadata_with_context_and_model_client(
|
||||
worker_name,
|
||||
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));
|
||||
}
|
||||
Ok(PreparedWorker::new(worker, self.layout, self.transport))
|
||||
}
|
||||
|
||||
pub async fn start(self) -> Result<BootstrappedWorker, WorkerBootstrapError> {
|
||||
self.prepare().await?.start().await
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, St> PreparedWorker<C, St>
|
||||
where
|
||||
C: LlmClient + Clone + 'static,
|
||||
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
|
||||
{
|
||||
/// Wrap a restored Worker in the same pre-exposure lifecycle used by fresh
|
||||
/// bootstraps.
|
||||
pub fn new(
|
||||
worker: Worker<C, St>,
|
||||
layout: WorkerBootstrapLayout,
|
||||
transport: WorkerControllerTransport,
|
||||
) -> Self {
|
||||
Self {
|
||||
worker,
|
||||
layout,
|
||||
transport,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn worker(&self) -> &Worker<C, St> {
|
||||
&self.worker
|
||||
}
|
||||
|
||||
pub fn worker_mut(&mut self) -> &mut Worker<C, St> {
|
||||
&mut self.worker
|
||||
}
|
||||
|
||||
pub async fn start(self) -> Result<BootstrappedWorker, WorkerBootstrapError> {
|
||||
start_worker_controller(self.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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +237,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
|
||||
|
||||
@@ -14,9 +14,9 @@ use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::runtime_command::WorkerRuntimeCommand;
|
||||
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use async_trait::async_trait;
|
||||
use client::WorkerRuntimeCommand;
|
||||
use manifest::{Permission, ScopeRule};
|
||||
use protocol::stream::JsonLineReader;
|
||||
use protocol::{Event, Method, WorkerStatus};
|
||||
|
||||
@@ -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};
|
||||
@@ -184,15 +184,16 @@ fn load_spawn_config_json(
|
||||
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
|
||||
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
|
||||
let manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(config))
|
||||
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
||||
let manifest =
|
||||
WorkerManifest::try_from(WorkerManifestConfig::resolution_defaults().merge(config))
|
||||
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
||||
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||
}
|
||||
|
||||
fn load_builtin_default_manifest(
|
||||
worker_name: &str,
|
||||
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||
let mut config = WorkerManifestConfig::builtin_defaults();
|
||||
let mut config = WorkerManifestConfig::resolution_defaults();
|
||||
config.worker.name = Some(worker_name.to_string());
|
||||
let manifest = WorkerManifest::try_from(config)
|
||||
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
|
||||
@@ -259,7 +260,7 @@ fn load_single_manifest(
|
||||
absolute_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut config = WorkerManifestConfig::builtin_defaults().merge(
|
||||
let mut config = WorkerManifestConfig::resolution_defaults().merge(
|
||||
WorkerManifestConfig::from_toml(&toml)
|
||||
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?
|
||||
.resolve_paths(base_dir),
|
||||
@@ -633,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;
|
||||
@@ -10,6 +11,7 @@ pub mod ipc;
|
||||
pub mod model_client;
|
||||
pub mod prompt;
|
||||
pub mod runtime;
|
||||
pub mod runtime_command;
|
||||
pub mod segment_log_sink;
|
||||
mod session_capture;
|
||||
mod session_history;
|
||||
@@ -23,6 +25,10 @@ mod interrupt_prep;
|
||||
mod permission;
|
||||
mod worker;
|
||||
|
||||
pub use bootstrap::{
|
||||
BootstrappedWorker, PreparedWorker, 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};
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::ffi::OsString;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const POD_RUNTIME_COMMAND_ENV: &str = "YOI_POD_RUNTIME_COMMAND";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct WorkerRuntimeCommand {
|
||||
pub program: PathBuf,
|
||||
pub prefix_args: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl WorkerRuntimeCommand {
|
||||
pub fn new(program: impl Into<PathBuf>, prefix_args: Vec<OsString>) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
prefix_args,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_current_exe() -> io::Result<Self> {
|
||||
Ok(Self::for_executable(std::env::current_exe()?))
|
||||
}
|
||||
|
||||
pub fn for_executable(program: impl Into<PathBuf>) -> Self {
|
||||
Self::new(program, vec![OsString::from("worker")])
|
||||
}
|
||||
|
||||
/// Resolve the Worker runtime command used for subprocess launches.
|
||||
///
|
||||
/// The default launch path is always the current `yoi` executable plus
|
||||
/// the unified `worker` prefix argument. During development, a non-empty
|
||||
/// `YOI_POD_RUNTIME_COMMAND` value replaces only the executable path;
|
||||
/// the `worker` prefix is still added here and the env value is not parsed as a
|
||||
/// shell command.
|
||||
pub fn resolve() -> io::Result<Self> {
|
||||
Self::resolve_from_env_value(
|
||||
std::env::var_os(POD_RUNTIME_COMMAND_ENV),
|
||||
std::env::current_exe,
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_from_env_value<F>(
|
||||
override_program: Option<OsString>,
|
||||
current_exe: F,
|
||||
) -> io::Result<Self>
|
||||
where
|
||||
F: FnOnce() -> io::Result<PathBuf>,
|
||||
{
|
||||
if let Some(program) = override_program.filter(|program| !program.as_os_str().is_empty()) {
|
||||
return Ok(Self::for_executable(program));
|
||||
}
|
||||
|
||||
Ok(Self::for_executable(current_exe()?))
|
||||
}
|
||||
|
||||
pub fn program(&self) -> &Path {
|
||||
&self.program
|
||||
}
|
||||
|
||||
pub fn prefix_args(&self) -> &[OsString] {
|
||||
&self.prefix_args
|
||||
}
|
||||
|
||||
pub fn argv_with<I, S>(&self, args: I) -> Vec<OsString>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let mut argv = self.prefix_args.clone();
|
||||
argv.extend(args.into_iter().map(Into::into));
|
||||
argv
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for WorkerRuntimeCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.program.display())?;
|
||||
for arg in &self.prefix_args {
|
||||
write!(f, " {}", arg.to_string_lossy())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn yoi_binary_defaults_to_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi");
|
||||
|
||||
assert_eq!(command.program(), Path::new("/opt/yoi/bin/yoi"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_runtime_executable_gets_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::for_executable("/opt/yoi/bin/custom-runtime");
|
||||
|
||||
assert_eq!(command.program(), Path::new("/opt/yoi/bin/custom-runtime"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_current_exe_when_override_is_unset() {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(None, || {
|
||||
Ok(PathBuf::from("/opt/yoi/bin/yoi"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_current_exe_when_override_is_empty() {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || {
|
||||
Ok(PathBuf::from("/opt/yoi/bin/yoi"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_override_replaces_only_program_and_keeps_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(
|
||||
Some(OsString::from("/tmp/rebuilt yoi")),
|
||||
|| panic!("override must not inspect current_exe"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(command.program(), Path::new("/tmp/rebuilt yoi"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -403,11 +403,10 @@ impl Tool for SubWorkerSpawnTool {
|
||||
allow: scope_allow.clone(),
|
||||
deny: Vec::new(),
|
||||
};
|
||||
let mut child_manifest =
|
||||
WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(child_config))
|
||||
.map_err(|error| {
|
||||
ToolError::ExecutionFailed(format!("resolve child manifest: {error}"))
|
||||
})?;
|
||||
let mut child_manifest = WorkerManifest::try_from(
|
||||
WorkerManifestConfig::resolution_defaults().merge(child_config),
|
||||
)
|
||||
.map_err(|error| ToolError::ExecutionFailed(format!("resolve child manifest: {error}")))?;
|
||||
// Delegated children stay bound to their scoped session and cannot use
|
||||
// Workspace attachment tools to replace it with parent-level authority.
|
||||
child_manifest.feature.manage_workdir.enabled = false;
|
||||
@@ -1625,7 +1624,7 @@ max_tokens = 3333
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
|
||||
let manifest: WorkerManifest = WorkerManifestConfig::resolution_defaults()
|
||||
.merge(parsed)
|
||||
.try_into()
|
||||
.unwrap();
|
||||
@@ -1845,7 +1844,7 @@ max_tokens = 3333
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ambiguous_and_no_default_diagnostics_include_available_selectors() {
|
||||
fn invalid_and_ambiguous_diagnostics_include_available_selectors() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let project = tmp.path().join("project");
|
||||
std::fs::create_dir_all(&project).unwrap();
|
||||
@@ -1863,17 +1862,21 @@ max_tokens = 3333
|
||||
assert!(invalid.contains("Use `default`, `inherit`"));
|
||||
assert!(invalid.contains("`project:coder`"));
|
||||
|
||||
let default_error = build_spawn_config_json_for_profile(
|
||||
&parent,
|
||||
&available,
|
||||
&project,
|
||||
"child",
|
||||
None,
|
||||
&scope,
|
||||
SpawnProfileSelector::Default,
|
||||
let default_config: serde_json::Value = serde_json::from_str(
|
||||
&build_spawn_config_json_for_profile(
|
||||
&parent,
|
||||
&available,
|
||||
&project,
|
||||
"child",
|
||||
None,
|
||||
&scope,
|
||||
SpawnProfileSelector::Default,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(default_error.contains("no default profile is configured"));
|
||||
.unwrap();
|
||||
assert_eq!(default_config["feature"]["sub_worker"]["enabled"], true);
|
||||
assert_eq!(default_config["feature"]["ticket"]["enabled"], false);
|
||||
|
||||
let user_config = tmp.path().join("user-profiles.toml");
|
||||
std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.toml\"\n").unwrap();
|
||||
|
||||
@@ -5127,15 +5127,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
|
||||
@@ -5515,6 +5535,27 @@ where
|
||||
loader: PromptCatalogSource,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
Self::restore_pending_from_worker_metadata_with_context_and_model_client(
|
||||
worker_name,
|
||||
fallback,
|
||||
store,
|
||||
loader,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn restore_pending_from_worker_metadata_with_context_and_model_client(
|
||||
worker_name: &str,
|
||||
fallback: WorkerManifest,
|
||||
store: St,
|
||||
loader: PromptCatalogSource,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
model_client: Option<Box<dyn LlmClient>>,
|
||||
) -> Result<Self, WorkerError> {
|
||||
let metadata =
|
||||
store
|
||||
@@ -5535,7 +5576,7 @@ where
|
||||
worker_name: worker_name.to_string(),
|
||||
})?;
|
||||
if let Some(segment_id) = active.segment_id {
|
||||
return Self::restore_from_manifest_with_context(
|
||||
return Self::restore_from_manifest_with_context_and_model_client(
|
||||
active.session_id,
|
||||
segment_id,
|
||||
restore_manifest_from_worker_metadata_snapshot(
|
||||
@@ -5547,6 +5588,7 @@ where
|
||||
loader,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
model_client,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -5557,12 +5599,13 @@ where
|
||||
})?;
|
||||
let manifest =
|
||||
restore_manifest_from_worker_metadata_snapshot(worker_name, Some(snapshot), fallback)?;
|
||||
Self::from_manifest_with_context(
|
||||
Self::from_manifest_with_context_and_model_client(
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
model_client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -5614,6 +5657,29 @@ where
|
||||
loader: PromptCatalogSource,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
) -> Result<Self, WorkerError> {
|
||||
Self::restore_from_manifest_with_context_and_model_client(
|
||||
session_id,
|
||||
segment_id,
|
||||
manifest,
|
||||
store,
|
||||
loader,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn restore_from_manifest_with_context_and_model_client(
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
manifest: WorkerManifest,
|
||||
store: St,
|
||||
loader: PromptCatalogSource,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
filesystem_authority: WorkerFilesystemAuthority,
|
||||
model_client: Option<Box<dyn LlmClient>>,
|
||||
) -> Result<Self, WorkerError> {
|
||||
// Read raw entries once so we can both reconstruct state and
|
||||
// seed the broadcast sink's mirror with the same prefix that
|
||||
@@ -5629,13 +5695,14 @@ where
|
||||
let mirror_entries: Vec<LogEntry> = raw_entries.clone();
|
||||
let scope_config = effective_restore_scope_config(&store, &manifest)?;
|
||||
|
||||
let common = prepare_worker_common_with_context(
|
||||
let common = prepare_worker_common_with_context_and_model_client(
|
||||
&manifest,
|
||||
&loader,
|
||||
/* parse_template */ false,
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
scope_config,
|
||||
model_client,
|
||||
)?;
|
||||
|
||||
// Atomic: register_worker inside install_top_level rejects when
|
||||
@@ -6610,6 +6677,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,
|
||||
@@ -6645,6 +6732,7 @@ fn prepare_worker_common_with_context(
|
||||
workspace_context,
|
||||
filesystem_authority,
|
||||
scope,
|
||||
model_client,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6655,6 +6743,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) {
|
||||
@@ -6671,7 +6760,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