worker: replace filesystem prompts with effective DCDL catalog
This commit is contained in:
@@ -1028,7 +1028,7 @@ profile = "builtin:companion"
|
|||||||
r#"
|
r#"
|
||||||
[ticket.roles.reviewer]
|
[ticket.roles.reviewer]
|
||||||
profile = "builtin:companion"
|
profile = "builtin:companion"
|
||||||
launch_prompt = "$workspace/ticket/reviewer/launch"
|
launch_prompt = "ticket.reviewer.launch"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
||||||
@@ -1043,11 +1043,11 @@ launch_prompt = "$workspace/ticket/reviewer/launch"
|
|||||||
assert_eq!(plan.profile, "builtin:companion");
|
assert_eq!(plan.profile, "builtin:companion");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan.launch_prompt_ref.as_deref(),
|
plan.launch_prompt_ref.as_deref(),
|
||||||
Some("$workspace/ticket/reviewer/launch")
|
Some("ticket.reviewer.launch")
|
||||||
);
|
);
|
||||||
assert!(matches!(&plan.run_segments[0], Segment::Text { .. }));
|
assert!(matches!(&plan.run_segments[0], Segment::Text { .. }));
|
||||||
assert!(!text.contains("Configured launch_prompt"));
|
assert!(!text.contains("Configured launch_prompt"));
|
||||||
assert!(!text.contains("$workspace/ticket/reviewer/launch"));
|
assert!(!text.contains("ticket.reviewer.launch"));
|
||||||
assert!(!text.contains("Profile selector: builtin:companion"));
|
assert!(!text.contains("Profile selector: builtin:companion"));
|
||||||
assert!(!text.contains("Role: reviewer"));
|
assert!(!text.contains("Role: reviewer"));
|
||||||
assert!(!text.contains("system_instruction"));
|
assert!(!text.contains("system_instruction"));
|
||||||
@@ -1228,7 +1228,7 @@ profile = "./coder.toml"
|
|||||||
r#"
|
r#"
|
||||||
[ticket.roles.coder]
|
[ticket.roles.coder]
|
||||||
profile = "inherit"
|
profile = "inherit"
|
||||||
system_instruction = "$workspace/not-supported"
|
system_instruction = "unsupported"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||||
|
|||||||
@@ -349,11 +349,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
|
|||||||
pub struct WorkerMetaConfig {
|
pub struct WorkerMetaConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
/// Optional `PromptCatalog` manifest pack override. See
|
|
||||||
/// [`crate::WorkerMeta::prompt_pack`] for semantics. Relative paths
|
|
||||||
/// are resolved through [`WorkerManifestConfig::resolve_paths`].
|
|
||||||
#[serde(default)]
|
|
||||||
pub prompt_pack: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
@@ -554,9 +549,6 @@ impl WorkerManifestConfig {
|
|||||||
base.display()
|
base.display()
|
||||||
);
|
);
|
||||||
resolve_auth_file(&mut self.model.auth, base);
|
resolve_auth_file(&mut self.model.auth, base);
|
||||||
if let Some(ref mut pack) = self.worker.prompt_pack {
|
|
||||||
*pack = join_if_relative(base, pack);
|
|
||||||
}
|
|
||||||
for rule in &mut self.scope.allow {
|
for rule in &mut self.scope.allow {
|
||||||
rule.target = join_if_relative(base, &rule.target);
|
rule.target = join_if_relative(base, &rule.target);
|
||||||
}
|
}
|
||||||
@@ -718,7 +710,6 @@ impl WorkerMetaConfig {
|
|||||||
fn merge(self, upper: Self) -> Self {
|
fn merge(self, upper: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: upper.name.or(self.name),
|
name: upper.name.or(self.name),
|
||||||
prompt_pack: upper.prompt_pack.or(self.prompt_pack),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,10 +1009,6 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
|
|||||||
.worker
|
.worker
|
||||||
.name
|
.name
|
||||||
.ok_or(ResolveError::MissingField("worker.name"))?;
|
.ok_or(ResolveError::MissingField("worker.name"))?;
|
||||||
let prompt_pack = cfg.worker.prompt_pack;
|
|
||||||
if let Some(ref p) = prompt_pack {
|
|
||||||
ensure_absolute("worker.prompt_pack", p)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_model_paths(&cfg.model, "model.auth.file")?;
|
validate_model_paths(&cfg.model, "model.auth.file")?;
|
||||||
|
|
||||||
@@ -1150,7 +1137,7 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
|
|||||||
validate_mcp_config(&cfg.mcp)?;
|
validate_mcp_config(&cfg.mcp)?;
|
||||||
|
|
||||||
Ok(WorkerManifest {
|
Ok(WorkerManifest {
|
||||||
worker: WorkerMeta { name, prompt_pack },
|
worker: WorkerMeta { name },
|
||||||
model: cfg.model,
|
model: cfg.model,
|
||||||
engine,
|
engine,
|
||||||
scope: cfg.scope,
|
scope: cfg.scope,
|
||||||
@@ -1187,7 +1174,6 @@ mod tests {
|
|||||||
WorkerManifestConfig {
|
WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("test".into()),
|
name: Some("test".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
@@ -1505,7 +1491,6 @@ mod tests {
|
|||||||
let lower = WorkerManifestConfig {
|
let lower = WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("lower".into()),
|
name: Some("lower".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
model_id: Some("lower-model".into()),
|
model_id: Some("lower-model".into()),
|
||||||
@@ -1516,7 +1501,6 @@ mod tests {
|
|||||||
let upper = WorkerManifestConfig {
|
let upper = WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("upper".into()),
|
name: Some("upper".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1925,7 +1909,6 @@ enabled = false
|
|||||||
.merge(WorkerManifestConfig {
|
.merge(WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("feature-test".into()),
|
name: Some("feature-test".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
@@ -2008,7 +1991,6 @@ enabled = true
|
|||||||
.merge(WorkerManifestConfig {
|
.merge(WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("feature-merge-test".into()),
|
name: Some("feature-merge-test".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
@@ -2075,7 +2057,6 @@ permission = "write"
|
|||||||
let overlay = WorkerManifestConfig {
|
let overlay = WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("x".into()),
|
name: Some("x".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
|
|||||||
@@ -42,10 +42,9 @@ pub const COMPACT_OVERVIEW_WARNING_TOKENS: u64 = 16_000;
|
|||||||
/// See [`crate::CompactionConfig::overview_deadline_tokens`].
|
/// See [`crate::CompactionConfig::overview_deadline_tokens`].
|
||||||
pub const COMPACT_OVERVIEW_DEADLINE_TOKENS: u64 = 40_000;
|
pub const COMPACT_OVERVIEW_DEADLINE_TOKENS: u64 = 40_000;
|
||||||
|
|
||||||
/// Default instruction asset reference used when `worker.instruction`
|
/// Default exact catalog-root dotted Prompt name used when
|
||||||
/// is omitted. See the `PromptLoader` prefix addressing scheme for the
|
/// `worker.instruction` is omitted.
|
||||||
/// `$yoi/` / `$user/` / `$workspace/` namespaces.
|
pub const DEFAULT_INSTRUCTION: &str = "default";
|
||||||
pub const DEFAULT_INSTRUCTION: &str = "$yoi/default";
|
|
||||||
|
|
||||||
/// Default language policy used by the main worker for normal prose
|
/// Default language policy used by the main worker for normal prose
|
||||||
/// responses. See [`crate::EngineManifest::language`].
|
/// responses. See [`crate::EngineManifest::language`].
|
||||||
|
|||||||
@@ -500,29 +500,13 @@ pub struct MemoryConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkerMeta {
|
pub struct WorkerMeta {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Optional path to a TOML override file read as the top layer of
|
|
||||||
/// `worker::PromptCatalog`. Subject to the same relative-path
|
|
||||||
/// resolution as other manifest paths (joined against the
|
|
||||||
/// manifest's base directory). `None` leaves the 4th overlay layer
|
|
||||||
/// empty; auto-discovered user and workspace packs still apply.
|
|
||||||
///
|
|
||||||
/// Note: unlike `worker.instruction`, this is a plain filesystem
|
|
||||||
/// path — not a `$prefix/` prompt reference. Pack files carry
|
|
||||||
/// structured TOML data, while `worker.instruction` points at a
|
|
||||||
/// minijinja `.md` template; the two use different addressing
|
|
||||||
/// conventions on purpose.
|
|
||||||
#[serde(default)]
|
|
||||||
pub prompt_pack: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Worker-level configuration embedded in the manifest.
|
/// Worker-level configuration embedded in the manifest.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EngineManifest {
|
pub struct EngineManifest {
|
||||||
/// Reference to the instruction prompt asset used as the body of
|
/// Exact catalog-root dotted Prompt name (for example `default` or
|
||||||
/// the worker's system prompt. Uses the `PromptLoader` prefix
|
/// `role.coder`).
|
||||||
/// addressing scheme (`$yoi/...`, `$user/...`,
|
|
||||||
/// `$workspace/...`) and is always populated after resolution —
|
|
||||||
/// unset manifests fall through to [`defaults::DEFAULT_INSTRUCTION`].
|
|
||||||
#[serde(default = "default_instruction")]
|
#[serde(default = "default_instruction")]
|
||||||
pub instruction: String,
|
pub instruction: String,
|
||||||
/// Language policy used by the main worker for normal prose responses.
|
/// Language policy used by the main worker for normal prose responses.
|
||||||
@@ -959,7 +943,7 @@ model_id = "claude-sonnet-4-20250514"
|
|||||||
auth = { kind = "api_key", file = "/abs/keys/anthropic" }
|
auth = { kind = "api_key", file = "/abs/keys/anthropic" }
|
||||||
|
|
||||||
[engine]
|
[engine]
|
||||||
instruction = "$user/reviewer"
|
instruction = "role.reviewer"
|
||||||
max_tokens = 4096
|
max_tokens = 4096
|
||||||
temperature = 0.3
|
temperature = 0.3
|
||||||
top_p = 0.9
|
top_p = 0.9
|
||||||
@@ -995,7 +979,7 @@ permission = "write"
|
|||||||
_ => panic!("expected ApiKey"),
|
_ => panic!("expected ApiKey"),
|
||||||
};
|
};
|
||||||
assert_eq!(file, Some(std::path::Path::new("/abs/keys/anthropic")));
|
assert_eq!(file, Some(std::path::Path::new("/abs/keys/anthropic")));
|
||||||
assert_eq!(manifest.engine.instruction, "$user/reviewer");
|
assert_eq!(manifest.engine.instruction, "role.reviewer");
|
||||||
assert_eq!(manifest.engine.max_tokens, Some(4096));
|
assert_eq!(manifest.engine.max_tokens, Some(4096));
|
||||||
assert_eq!(manifest.engine.temperature, Some(0.3));
|
assert_eq!(manifest.engine.temperature, Some(0.3));
|
||||||
assert_eq!(manifest.engine.top_p, Some(0.9));
|
assert_eq!(manifest.engine.top_p, Some(0.9));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! 用途別に三つの base directory を持つ:
|
//! 用途別に三つの base directory を持つ:
|
||||||
//!
|
//!
|
||||||
//! - **`config_dir`** — 人が手で書く / 編集する設定。`profiles.toml`,
|
//! - **`config_dir`** — 人が手で書く / 編集する設定。`profiles.toml`,
|
||||||
//! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等
|
//! `providers.toml`, `models.toml` 等
|
||||||
//! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等
|
//! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等
|
||||||
//! - **`secret_data_dir`** — local secret store の読み書き base。既存
|
//! - **`secret_data_dir`** — local secret store の読み書き base。既存
|
||||||
//! secret store は path-derived key を使うため、通常 data とは別に
|
//! secret store は path-derived key を使うため、通常 data とは別に
|
||||||
@@ -85,16 +85,6 @@ pub fn user_profiles_path() -> Option<PathBuf> {
|
|||||||
user_profiles_path_from_config_dir(config_dir())
|
user_profiles_path_from_config_dir(config_dir())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
|
||||||
pub fn user_prompts_dir() -> Option<PathBuf> {
|
|
||||||
user_prompts_dir_from_config_dir(config_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `<config_dir>/prompts.toml` — user prompt pack。
|
|
||||||
pub fn user_pack_file() -> Option<PathBuf> {
|
|
||||||
user_pack_file_from_config_dir(config_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `<config_dir>/<file_name>` — providers.toml / models.toml 等の
|
/// `<config_dir>/<file_name>` — providers.toml / models.toml 等の
|
||||||
/// user override ファイル。
|
/// user override ファイル。
|
||||||
pub fn user_catalog_override(file_name: &str) -> Option<PathBuf> {
|
pub fn user_catalog_override(file_name: &str) -> Option<PathBuf> {
|
||||||
@@ -200,14 +190,6 @@ fn user_profiles_path_from_config_dir(config_dir: Option<PathBuf>) -> Option<Pat
|
|||||||
Some(config_dir?.join("profiles.toml"))
|
Some(config_dir?.join("profiles.toml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_prompts_dir_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
|
|
||||||
Some(config_dir?.join("prompts"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_pack_file_from_config_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
|
|
||||||
Some(config_dir?.join("prompts.toml"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_catalog_override_from_config_dir(
|
fn user_catalog_override_from_config_dir(
|
||||||
config_dir: Option<PathBuf>,
|
config_dir: Option<PathBuf>,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
@@ -465,14 +447,6 @@ mod tests {
|
|||||||
user_profiles_path_from_config_dir(config_dir.clone()).unwrap(),
|
user_profiles_path_from_config_dir(config_dir.clone()).unwrap(),
|
||||||
PathBuf::from("/sand/config/profiles.toml")
|
PathBuf::from("/sand/config/profiles.toml")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
user_prompts_dir_from_config_dir(config_dir.clone()).unwrap(),
|
|
||||||
PathBuf::from("/sand/config/prompts")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
user_pack_file_from_config_dir(config_dir.clone()).unwrap(),
|
|
||||||
PathBuf::from("/sand/config/prompts.toml")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(),
|
user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(),
|
||||||
PathBuf::from("/sand/config/providers.toml")
|
PathBuf::from("/sand/config/providers.toml")
|
||||||
|
|||||||
@@ -547,7 +547,6 @@ fn resolve_profile_value(
|
|||||||
let config = WorkerManifestConfig {
|
let config = WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some(worker_name),
|
name: Some(worker_name),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: profile.model.unwrap_or_default(),
|
model: profile.model.unwrap_or_default(),
|
||||||
engine: profile.engine.unwrap_or_default(),
|
engine: profile.engine.unwrap_or_default(),
|
||||||
|
|||||||
@@ -1047,19 +1047,19 @@ worktree_name = "custom-orchestrator"
|
|||||||
|
|
||||||
[ticket.roles.intake]
|
[ticket.roles.intake]
|
||||||
profile = "project:intake"
|
profile = "project:intake"
|
||||||
launch_prompt = "$workspace/ticket/intake/launch"
|
launch_prompt = "ticket.intake.launch"
|
||||||
|
|
||||||
[ticket.roles.orchestrator]
|
[ticket.roles.orchestrator]
|
||||||
profile = "project:orchestrator"
|
profile = "project:orchestrator"
|
||||||
launch_prompt = "$workspace/ticket/orchestrator/launch"
|
launch_prompt = "ticket.orchestrator.launch"
|
||||||
|
|
||||||
[ticket.roles.coder]
|
[ticket.roles.coder]
|
||||||
profile = "inherit"
|
profile = "inherit"
|
||||||
launch_prompt = "$workspace/ticket/coder/launch"
|
launch_prompt = "ticket.coder.launch"
|
||||||
|
|
||||||
[ticket.roles.reviewer]
|
[ticket.roles.reviewer]
|
||||||
profile = "project:reviewer"
|
profile = "project:reviewer"
|
||||||
launch_prompt = "$workspace/ticket/reviewer/launch"
|
launch_prompt = "ticket.reviewer.launch"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1095,7 +1095,7 @@ launch_prompt = "$workspace/ticket/reviewer/launch"
|
|||||||
.launch_prompt_for(TicketRole::Reviewer)
|
.launch_prompt_for(TicketRole::Reviewer)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_str(),
|
.as_str(),
|
||||||
"$workspace/ticket/reviewer/launch"
|
"ticket.reviewer.launch"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1338,7 +1338,7 @@ profile = "builtin:companion"
|
|||||||
r#"
|
r#"
|
||||||
[roles.coder]
|
[roles.coder]
|
||||||
profile = "inherit"
|
profile = "inherit"
|
||||||
system_instruction = "$workspace/not-supported"
|
system_instruction = "unsupported"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub struct ConfigBundle {
|
|||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub declarations: Vec<ConfigDeclaration>,
|
pub declarations: Vec<ConfigDeclaration>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub prompt_catalog: Option<worker::EffectivePromptCatalog>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub profile_source_archive: Option<ProfileSourceArchive>,
|
pub profile_source_archive: Option<ProfileSourceArchive>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub profile_source_archive_handle: Option<BackendResourceHandle>,
|
pub profile_source_archive_handle: Option<BackendResourceHandle>,
|
||||||
@@ -69,6 +71,16 @@ impl ConfigBundle {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(prompt_catalog) = &self.prompt_catalog {
|
||||||
|
lines.push(format!(
|
||||||
|
"prompt_catalog\0{}\0{}\0{}\0{}",
|
||||||
|
prompt_catalog.config_revision,
|
||||||
|
prompt_catalog.schema_fingerprint,
|
||||||
|
prompt_catalog.toolchain_fingerprint,
|
||||||
|
prompt_catalog.catalog_digest
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(archive) = &self.profile_source_archive {
|
if let Some(archive) = &self.profile_source_archive {
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"profile_archive\0{}\0{}\0{}",
|
"profile_archive\0{}\0{}\0{}",
|
||||||
@@ -274,6 +286,12 @@ pub(crate) fn validate_config_bundle(bundle: &ConfigBundle) -> Result<(), Runtim
|
|||||||
validate_declaration_reference(&bundle.metadata.id, declaration)?;
|
validate_declaration_reference(&bundle.metadata.id, declaration)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(prompt_catalog) = &bundle.prompt_catalog {
|
||||||
|
prompt_catalog.verify_digest().map_err(|error| {
|
||||||
|
RuntimeError::InvalidRequest(format!("invalid Prompt catalog projection: {error}"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(archive) = &bundle.profile_source_archive {
|
if let Some(archive) = &bundle.profile_source_archive {
|
||||||
validate_profile_source_archive_ref(&archive.reference).map_err(|err| {
|
validate_profile_source_archive_ref(&archive.reference).map_err(|err| {
|
||||||
RuntimeError::InvalidRequest(format!("invalid profile source archive: {err}"))
|
RuntimeError::InvalidRequest(format!("invalid profile source archive: {err}"))
|
||||||
@@ -582,6 +600,7 @@ mod tests {
|
|||||||
name: "credential".to_string(),
|
name: "credential".to_string(),
|
||||||
reference: reference.to_string(),
|
reference: reference.to_string(),
|
||||||
}],
|
}],
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
@@ -615,6 +634,32 @@ mod tests {
|
|||||||
validate_config_bundle(&bundle_with_declaration("vault:team.api-key")).unwrap();
|
validate_config_bundle(&bundle_with_declaration("vault:team.api-key")).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_immutable_prompt_catalog_projection() {
|
||||||
|
let mut bundle = bundle_with_declaration("secret:github-token");
|
||||||
|
bundle.prompt_catalog = Some(
|
||||||
|
worker::EffectivePromptCatalog::new(
|
||||||
|
std::collections::BTreeMap::from([("default".to_string(), "hello".to_string())]),
|
||||||
|
7,
|
||||||
|
"schema",
|
||||||
|
"toolchain",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
bundle = bundle.with_computed_digest();
|
||||||
|
validate_config_bundle(&bundle).unwrap();
|
||||||
|
|
||||||
|
bundle
|
||||||
|
.prompt_catalog
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.templates
|
||||||
|
.insert("default".into(), "tampered".into());
|
||||||
|
bundle = bundle.with_computed_digest();
|
||||||
|
let error = validate_config_bundle(&bundle).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("catalog digest mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bundle_summary_redacts_runtime_internal_resource_handle() {
|
fn bundle_summary_redacts_runtime_internal_resource_handle() {
|
||||||
let mut bundle = bundle_with_declaration("secret:github-token");
|
let mut bundle = bundle_with_declaration("secret:github-token");
|
||||||
|
|||||||
@@ -1811,6 +1811,7 @@ mod tests {
|
|||||||
label: Some("test".to_string()),
|
label: Some("test".to_string()),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
declarations: Vec::new(),
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
@@ -2648,6 +2649,7 @@ mod ws_tests {
|
|||||||
label: Some("ws".to_string()),
|
label: Some("ws".to_string()),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
declarations: Vec::new(),
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,10 +743,10 @@ mod tests {
|
|||||||
.load(Some("profiles/main.dcdl"), "./shared.dcdl")
|
.load(Some("profiles/main.dcdl"), "./shared.dcdl")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match loaded {
|
match loaded {
|
||||||
LoadedImport::Source(source) => {
|
LoadedImport::Source { key, .. } => {
|
||||||
assert_eq!(source.key, "profiles/shared.dcdl");
|
assert_eq!(key, "profiles/shared.dcdl");
|
||||||
}
|
}
|
||||||
LoadedImport::Value(_) => panic!("expected source import"),
|
LoadedImport::Value { .. } => panic!("expected source import"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2832,6 +2832,7 @@ mod tests {
|
|||||||
name: "read".to_string(),
|
name: "read".to_string(),
|
||||||
reference: "capability:read".to_string(),
|
reference: "capability:read".to_string(),
|
||||||
}],
|
}],
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ use worker::feature::builtin::{
|
|||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
use worker::ipc::protocol_session::{live_log_entry_event, subscribe_worker_protocol_session};
|
use worker::ipc::protocol_session::{live_log_entry_event, subscribe_worker_protocol_session};
|
||||||
use worker::{
|
use worker::{
|
||||||
PromptLoader, SegmentLogSink, WORKER_INPUT_SUBMISSION_EXTENSION_DOMAIN, Worker,
|
PromptCatalogSource, SegmentLogSink, WORKER_INPUT_SUBMISSION_EXTENSION_DOMAIN, Worker,
|
||||||
WorkerController, WorkerControllerTransport, WorkerError, WorkerFilesystemAuthority,
|
WorkerController, WorkerControllerTransport, WorkerError, WorkerFilesystemAuthority,
|
||||||
WorkerHandle, WorkerSharedState, WorkerWorkspaceContext, WorkspaceClient, WorkspaceId,
|
WorkerHandle, WorkerSharedState, WorkerWorkspaceContext, WorkspaceClient, WorkspaceId,
|
||||||
};
|
};
|
||||||
@@ -329,12 +329,12 @@ impl ProfileRuntimeWorkerFactory {
|
|||||||
|
|
||||||
fn restore_fallback_manifest(
|
fn restore_fallback_manifest(
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(manifest::WorkerManifest, PromptLoader), String> {
|
) -> Result<(manifest::WorkerManifest, PromptCatalogSource), String> {
|
||||||
let mut config = manifest::WorkerManifestConfig::builtin_defaults();
|
let mut config = manifest::WorkerManifestConfig::builtin_defaults();
|
||||||
config.worker.name = Some(worker_name.to_string());
|
config.worker.name = Some(worker_name.to_string());
|
||||||
let manifest = manifest::WorkerManifest::try_from(config)
|
let manifest = manifest::WorkerManifest::try_from(config)
|
||||||
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
async fn resolve_profile_source_archive(
|
async fn resolve_profile_source_archive(
|
||||||
&self,
|
&self,
|
||||||
@@ -566,7 +566,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||||||
let archive = self
|
let archive = self
|
||||||
.resolve_profile_source_archive(&request.request.profile_source)
|
.resolve_profile_source_archive(&request.request.profile_source)
|
||||||
.await?;
|
.await?;
|
||||||
let (manifest, loader) = {
|
let (manifest, mut loader) = {
|
||||||
let manifest = archive
|
let manifest = archive
|
||||||
.resolve_profile(selector, &worker_root, &worker_name)
|
.resolve_profile(selector, &worker_root, &worker_name)
|
||||||
.map_err(|err| format!("failed to resolve profile source archive: {err}"))?;
|
.map_err(|err| format!("failed to resolve profile source archive: {err}"))?;
|
||||||
@@ -584,6 +584,13 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if let Some(prompt_catalog) = request
|
||||||
|
.config_bundle
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|bundle| bundle.prompt_catalog.clone())
|
||||||
|
{
|
||||||
|
loader = loader.with_effective_catalog(prompt_catalog);
|
||||||
|
}
|
||||||
let flow_transition_enabled = manifest.feature.flow.enabled;
|
let flow_transition_enabled = manifest.feature.flow.enabled;
|
||||||
|
|
||||||
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
|
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
|
||||||
@@ -719,7 +726,14 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||||||
self.worker_mutation_identity.as_ref(),
|
self.worker_mutation_identity.as_ref(),
|
||||||
self.embedded_worker_mutation_dispatcher.as_ref(),
|
self.embedded_worker_mutation_dispatcher.as_ref(),
|
||||||
);
|
);
|
||||||
let (manifest, loader) = Self::restore_fallback_manifest(&worker_name)?;
|
let (manifest, mut loader) = Self::restore_fallback_manifest(&worker_name)?;
|
||||||
|
if let Some(prompt_catalog) = request
|
||||||
|
.config_bundle
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|bundle| bundle.prompt_catalog.clone())
|
||||||
|
{
|
||||||
|
loader = loader.with_effective_catalog(prompt_catalog);
|
||||||
|
}
|
||||||
|
|
||||||
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
|
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
|
||||||
let session_dir = worker_aggregate_dir.join("session");
|
let session_dir = worker_aggregate_dir.join("session");
|
||||||
@@ -2090,6 +2104,7 @@ mod tests {
|
|||||||
label: Some("adapter-test".to_string()),
|
label: Some("adapter-test".to_string()),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
declarations: Vec::new(),
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: Some(sample_profile_archive()),
|
profile_source_archive: Some(sample_profile_archive()),
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ tools = { workspace = true }
|
|||||||
workdir = { workspace = true }
|
workdir = { workspace = true }
|
||||||
minijinja = "2.19.0"
|
minijinja = "2.19.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
config-source = { path = "../config-source" }
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
fs4 = { workspace = true, features = ["sync"] }
|
fs4 = { workspace = true, features = ["sync"] }
|
||||||
flow = { path = "../flow" }
|
flow = { path = "../flow" }
|
||||||
@@ -51,6 +52,3 @@ serial_test = "3.4.0"
|
|||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
wat = "1.241.2"
|
wat = "1.241.2"
|
||||||
yoi-plugin-pdk = { workspace = true }
|
yoi-plugin-pdk = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
toml = { workspace = true }
|
|
||||||
|
|||||||
+1
-47
@@ -1,49 +1,3 @@
|
|||||||
//! Emits `$OUT_DIR/internal_keys.rs` containing the sorted list of keys
|
|
||||||
//! present in `resources/prompts/internal.toml`. The generated slice is
|
|
||||||
//! included into `src/prompts.rs` where a `const _` assertion compares
|
|
||||||
//! it bidirectionally against the `WorkerPrompt` enum's own key list, so
|
|
||||||
//! that a mismatch fails the build (see ticket: worker-prompt-catalog).
|
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
println!("cargo:rerun-if-changed=../../resources/prompts");
|
||||||
let toml_path = PathBuf::from(&manifest_dir)
|
|
||||||
.join("..")
|
|
||||||
.join("..")
|
|
||||||
.join("resources")
|
|
||||||
.join("prompts")
|
|
||||||
.join("internal.toml");
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed={}", toml_path.display());
|
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
|
||||||
|
|
||||||
let toml_str = fs::read_to_string(&toml_path)
|
|
||||||
.unwrap_or_else(|e| panic!("failed to read {}: {e}", toml_path.display()));
|
|
||||||
|
|
||||||
let parsed: toml::Value = toml::from_str(&toml_str)
|
|
||||||
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", toml_path.display()));
|
|
||||||
|
|
||||||
let prompt_section = parsed
|
|
||||||
.get("prompt")
|
|
||||||
.and_then(|v| v.as_table())
|
|
||||||
.unwrap_or_else(|| panic!("{} must contain a `[prompt]` table", toml_path.display()));
|
|
||||||
|
|
||||||
let mut keys: Vec<String> = prompt_section.keys().cloned().collect();
|
|
||||||
keys.sort();
|
|
||||||
|
|
||||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
|
|
||||||
let out_path = PathBuf::from(out_dir).join("internal_keys.rs");
|
|
||||||
|
|
||||||
let mut code = String::from("pub(crate) const INTERNAL_KEYS: &[&str] = &[\n");
|
|
||||||
for k in &keys {
|
|
||||||
code.push_str(" ");
|
|
||||||
code.push_str(&format!("{k:?}"));
|
|
||||||
code.push_str(",\n");
|
|
||||||
}
|
|
||||||
code.push_str("];\n");
|
|
||||||
|
|
||||||
fs::write(&out_path, code)
|
|
||||||
.unwrap_or_else(|e| panic!("failed to write {}: {e}", out_path.display()));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PromptLoader, Worker, WorkerController, WorkerFilesystemAuthority, WorkerWorkspaceContext,
|
PromptCatalogSource, Worker, WorkerController, WorkerFilesystemAuthority,
|
||||||
|
WorkerWorkspaceContext,
|
||||||
};
|
};
|
||||||
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};
|
||||||
@@ -137,7 +138,7 @@ fn sanitise_worker_name(raw: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_manifest(cli: &Cli) -> Result<(WorkerManifest, PromptLoader), String> {
|
fn resolve_manifest(cli: &Cli) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let process_root = runtime_workspace_root(cli)?;
|
let process_root = runtime_workspace_root(cli)?;
|
||||||
let runtime_worker_name = runtime_worker_name(cli, &process_root);
|
let runtime_worker_name = runtime_worker_name(cli, &process_root);
|
||||||
let ((mut manifest, loader), apply_direct_launch_policy) = if let Some(config_json) =
|
let ((mut manifest, loader), apply_direct_launch_policy) = if let Some(config_json) =
|
||||||
@@ -178,29 +179,31 @@ fn apply_session_restore_overrides(manifest: &mut WorkerManifest, cli: &Cli) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_spawn_config_json(config_json: &str) -> Result<(WorkerManifest, PromptLoader), String> {
|
fn load_spawn_config_json(
|
||||||
|
config_json: &str,
|
||||||
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
|
let config = serde_json::from_str::<WorkerManifestConfig>(config_json)
|
||||||
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
|
.map_err(|e| format!("failed to parse --spawn-config-json: {e}"))?;
|
||||||
let manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(config))
|
let manifest = WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(config))
|
||||||
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
.map_err(|e| format!("failed to resolve --spawn-config-json: {e}"))?;
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_builtin_default_manifest(
|
fn load_builtin_default_manifest(
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptLoader), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let mut config = WorkerManifestConfig::builtin_defaults();
|
let mut config = WorkerManifestConfig::builtin_defaults();
|
||||||
config.worker.name = Some(worker_name.to_string());
|
config.worker.name = Some(worker_name.to_string());
|
||||||
let manifest = WorkerManifest::try_from(config)
|
let manifest = WorkerManifest::try_from(config)
|
||||||
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
|
.map_err(|e| format!("failed to resolve builtin worker defaults: {e}"))?;
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_runtime_profile_manifest(
|
pub fn resolve_runtime_profile_manifest(
|
||||||
_profile: Option<&str>,
|
_profile: Option<&str>,
|
||||||
_workspace_root: &Path,
|
_workspace_root: &Path,
|
||||||
_worker_name: &str,
|
_worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptLoader), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
Err(
|
Err(
|
||||||
"runtime profile resolution requires a pre-resolved manifest/profile archive from Backend authority"
|
"runtime profile resolution requires a pre-resolved manifest/profile archive from Backend authority"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -211,7 +214,7 @@ pub fn resolve_runtime_profile_manifest_from_manifest(
|
|||||||
mut manifest: WorkerManifest,
|
mut manifest: WorkerManifest,
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptLoader), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
if manifest.worker.name.is_empty() {
|
if manifest.worker.name.is_empty() {
|
||||||
manifest.worker.name = worker_name.to_string();
|
manifest.worker.name = worker_name.to_string();
|
||||||
}
|
}
|
||||||
@@ -219,28 +222,28 @@ pub fn resolve_runtime_profile_manifest_from_manifest(
|
|||||||
// Do not run plugin discovery here: runtime-created Workers receive their
|
// Do not run plugin discovery here: runtime-created Workers receive their
|
||||||
// resolved manifest/profile archive from Backend authority, not by scanning
|
// resolved manifest/profile archive from Backend authority, not by scanning
|
||||||
// materialized workdir-local plugin stores.
|
// materialized workdir-local plugin stores.
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_runtime_profile_manifest_from_manifest_without_filesystem(
|
pub fn resolve_runtime_profile_manifest_from_manifest_without_filesystem(
|
||||||
mut manifest: WorkerManifest,
|
mut manifest: WorkerManifest,
|
||||||
_workspace_root: &Path,
|
_workspace_root: &Path,
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptLoader), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
if manifest.worker.name.is_empty() {
|
if manifest.worker.name.is_empty() {
|
||||||
manifest.worker.name = worker_name.to_string();
|
manifest.worker.name = worker_name.to_string();
|
||||||
}
|
}
|
||||||
manifest.scope = ScopeConfig::default();
|
manifest.scope = ScopeConfig::default();
|
||||||
manifest.delegation_scope = ScopeConfig::default();
|
manifest.delegation_scope = ScopeConfig::default();
|
||||||
// Same as the filesystem-capable runtime path: no local discovery.
|
// Same as the filesystem-capable runtime path: no local discovery.
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_single_manifest(
|
fn load_single_manifest(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
explicit_worker_name: Option<&str>,
|
explicit_worker_name: Option<&str>,
|
||||||
default_worker_name: &str,
|
default_worker_name: &str,
|
||||||
) -> Result<(WorkerManifest, PromptLoader), String> {
|
) -> Result<(WorkerManifest, PromptCatalogSource), String> {
|
||||||
let toml = std::fs::read_to_string(path)
|
let toml = std::fs::read_to_string(path)
|
||||||
.map_err(|e| format!("failed to read manifest {}: {e}", path.display()))?;
|
.map_err(|e| format!("failed to read manifest {}: {e}", path.display()))?;
|
||||||
let absolute_path = if path.is_absolute() {
|
let absolute_path = if path.is_absolute() {
|
||||||
@@ -274,7 +277,7 @@ fn load_single_manifest(
|
|||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok((manifest, PromptLoader::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_rule(target: PathBuf) -> ScopeRule {
|
fn read_rule(target: PathBuf) -> ScopeRule {
|
||||||
@@ -751,11 +754,9 @@ permission = "write"
|
|||||||
let cli =
|
let cli =
|
||||||
Cli::try_parse_from(["yoi worker", "--manifest", manifest.to_str().unwrap()]).unwrap();
|
Cli::try_parse_from(["yoi worker", "--manifest", manifest.to_str().unwrap()]).unwrap();
|
||||||
|
|
||||||
let (manifest, loader) = resolve_manifest(&cli).unwrap();
|
let (manifest, _loader) = resolve_manifest(&cli).unwrap();
|
||||||
|
|
||||||
assert_eq!(manifest.worker.name, "single");
|
assert_eq!(manifest.worker.name, "single");
|
||||||
assert!(loader.user_dir().is_none());
|
|
||||||
assert!(loader.workspace_dir().is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -834,12 +835,10 @@ language = "override"
|
|||||||
let cli = Cli::try_parse_from(["yoi worker", "--workspace", workspace.to_str().unwrap()])
|
let cli = Cli::try_parse_from(["yoi worker", "--workspace", workspace.to_str().unwrap()])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (manifest, loader) = resolve_manifest(&cli).unwrap();
|
let (manifest, _loader) = resolve_manifest(&cli).unwrap();
|
||||||
|
|
||||||
assert_eq!(manifest.worker.name, "runtime-workspace");
|
assert_eq!(manifest.worker.name, "runtime-workspace");
|
||||||
assert_ne!(manifest.engine.language, "override");
|
assert_ne!(manifest.engine.language, "override");
|
||||||
assert!(loader.user_dir().is_none());
|
|
||||||
assert!(loader.workspace_dir().is_none());
|
|
||||||
assert_scope_contains(&manifest.scope.allow, &workspace, Permission::Write);
|
assert_scope_contains(&manifest.scope.allow, &workspace, Permission::Write);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,12 +1030,8 @@ permission = "write"
|
|||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (manifest, loader) = resolve_manifest(&cli).unwrap();
|
let (manifest, _loader) = resolve_manifest(&cli).unwrap();
|
||||||
|
|
||||||
assert_eq!(manifest.worker.name, "single-file");
|
assert_eq!(manifest.worker.name, "single-file");
|
||||||
assert!(loader.user_dir().is_none());
|
|
||||||
assert!(loader.workspace_dir().is_none());
|
|
||||||
assert!(loader.user_pack_file().is_none());
|
|
||||||
assert!(loader.workspace_pack_file().is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1858,8 +1858,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instruction_contributions_are_deduped_in_registration_order() {
|
fn instruction_contributions_are_deduped_in_registration_order() {
|
||||||
let workflow = instruction("workflow", "$yoi/common/tickets");
|
let workflow = instruction("workflow", "common.tickets");
|
||||||
let orchestration = instruction("orchestration", "$yoi/common/worker-orchestration");
|
let orchestration = instruction("orchestration", "common.worker_orchestration");
|
||||||
let contributions = dedupe_instruction_contributions([
|
let contributions = dedupe_instruction_contributions([
|
||||||
workflow.clone(),
|
workflow.clone(),
|
||||||
orchestration.clone(),
|
orchestration.clone(),
|
||||||
@@ -1871,8 +1871,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undeclared_instruction_contribution_is_rejected() {
|
fn undeclared_instruction_contribution_is_rejected() {
|
||||||
let declared = instruction("declared", "$yoi/common/tickets");
|
let declared = instruction("declared", "common.tickets");
|
||||||
let undeclared = instruction("undeclared", "$yoi/common/tickets");
|
let undeclared = instruction("undeclared", "common.tickets");
|
||||||
let descriptor =
|
let descriptor =
|
||||||
FeatureDescriptor::builtin("instruction", "Instruction").with_instruction(declared);
|
FeatureDescriptor::builtin("instruction", "Instruction").with_instruction(declared);
|
||||||
let mut hook_builder = HookRegistryBuilder::default();
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const FEATURE_NAME: &str = "Ticket tools";
|
|||||||
const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \
|
const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \
|
||||||
The tools operate through the ticket crate backend and do not grant generic filesystem write scope.";
|
The tools operate through the ticket crate backend and do not grant generic filesystem write scope.";
|
||||||
const TICKET_WORKFLOW_INSTRUCTION_ID: &str = "ticket.workflow";
|
const TICKET_WORKFLOW_INSTRUCTION_ID: &str = "ticket.workflow";
|
||||||
const TICKET_WORKFLOW_PROMPT_REF: &str = "$yoi/common/tickets";
|
const TICKET_WORKFLOW_PROMPT_REF: &str = "common.tickets";
|
||||||
pub const TICKET_SERVICE_ID: &str = "ticket.authority";
|
pub const TICKET_SERVICE_ID: &str = "ticket.authority";
|
||||||
const TICKET_SERVICE_VERSION: &str = "1";
|
const TICKET_SERVICE_VERSION: &str = "1";
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const DEFAULT_PAGE_LIMIT: usize = 20;
|
|||||||
const MAX_PAGE_LIMIT: usize = 100;
|
const MAX_PAGE_LIMIT: usize = 100;
|
||||||
const MAX_READ_BYTES: usize = 16 * 1024;
|
const MAX_READ_BYTES: usize = 16 * 1024;
|
||||||
const OBSERVATION_INSTRUCTION_ID: &str = "worker-observation.policy";
|
const OBSERVATION_INSTRUCTION_ID: &str = "worker-observation.policy";
|
||||||
const OBSERVATION_PROMPT_REF: &str = "$yoi/common/worker-observation";
|
const OBSERVATION_PROMPT_REF: &str = "common.worker_observation";
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const OBSERVATION_PROMPT_SOURCE: &str =
|
const OBSERVATION_PROMPT_SOURCE: &str =
|
||||||
include_str!("../../../../../resources/prompts/common/worker-observation.md");
|
include_str!("../../../../../resources/prompts/common/worker-observation.md");
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ pub use manifest::{
|
|||||||
WorkerMetaConfig,
|
WorkerMetaConfig,
|
||||||
};
|
};
|
||||||
pub use model_client::{ProviderError, build_client};
|
pub use model_client::{ProviderError, build_client};
|
||||||
pub use prompt::catalog::{CatalogError, PromptCatalog, WorkerPrompt};
|
pub use prompt::catalog::{
|
||||||
pub use prompt::loader::PromptLoader;
|
CatalogError, EffectivePromptCatalog, PromptCatalog, WorkerPrompt, prompt_schema_source,
|
||||||
|
};
|
||||||
|
pub use prompt::source::PromptCatalogSource;
|
||||||
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
||||||
pub use protocol::{ErrorCode, Event, Method, TurnResult, WorkerStatus};
|
pub use protocol::{ErrorCode, Event, Method, TurnResult, WorkerStatus};
|
||||||
pub use runtime::dir::RuntimeDir;
|
pub use runtime::dir::RuntimeDir;
|
||||||
|
|||||||
+427
-587
File diff suppressed because it is too large
Load Diff
@@ -1,425 +0,0 @@
|
|||||||
//! Prefix-addressed prompt asset loader used by [`crate::SystemPromptTemplate`].
|
|
||||||
//!
|
|
||||||
//! Three prefixes address three physical libraries:
|
|
||||||
//!
|
|
||||||
//! | prefix | location |
|
|
||||||
//! |--------------|---------------------------------------------------------|
|
|
||||||
//! | `$yoi` | builtin, baked into the binary via `include_dir!` |
|
|
||||||
//! | `$user` | `<config_dir>/prompts/` (resolved by `manifest::paths`) |
|
|
||||||
//! | `$workspace` | `<project>/.yoi/prompts/` |
|
|
||||||
//!
|
|
||||||
//! A reference is `$<prefix>/<path>` where `<path>` is a `/`-separated
|
|
||||||
//! name without the `.md` extension (e.g. `$yoi/common/header`).
|
|
||||||
//! Unqualified names (no `$prefix/` at the front) are resolved relative
|
|
||||||
//! to an optional current reference — typically the file that issued
|
|
||||||
//! the `{% include %}` — so a prompt library can be authored as a
|
|
||||||
//! self-contained directory.
|
|
||||||
//!
|
|
||||||
//! Missing files produce a [`LoaderError::NotFound`]; there is no
|
|
||||||
//! fallthrough between layers.
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use include_dir::{Dir, include_dir};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
static BUILTIN_PROMPTS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../resources/prompts");
|
|
||||||
|
|
||||||
const PREFIX_YOI: &str = "$yoi";
|
|
||||||
const PREFIX_USER: &str = "$user";
|
|
||||||
const PREFIX_WORKSPACE: &str = "$workspace";
|
|
||||||
|
|
||||||
/// Prefix-resolved reference to a prompt asset. Produced by
|
|
||||||
/// [`PromptLoader::parse_ref`] from a user-supplied string such as
|
|
||||||
/// `"$yoi/default"`.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct PromptRef {
|
|
||||||
prefix: Prefix,
|
|
||||||
/// Relative path under the prefix root, without the `.md` extension.
|
|
||||||
/// `/`-separated, never empty, never starts with `/`.
|
|
||||||
path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Prefix {
|
|
||||||
Yoi,
|
|
||||||
User,
|
|
||||||
Workspace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Prefix {
|
|
||||||
fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Yoi => PREFIX_YOI,
|
|
||||||
Self::User => PREFIX_USER,
|
|
||||||
Self::Workspace => PREFIX_WORKSPACE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromptRef {
|
|
||||||
/// Produce a canonical `$prefix/path` string.
|
|
||||||
pub fn to_qualified_string(&self) -> String {
|
|
||||||
format!("{}/{}", self.prefix.as_str(), self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Directory portion (leading prefix segments minus the file name),
|
|
||||||
/// joined with `/`. Returns an empty string when the ref points at
|
|
||||||
/// a file directly under the prefix root.
|
|
||||||
fn dir(&self) -> &str {
|
|
||||||
match self.path.rsplit_once('/') {
|
|
||||||
Some((dir, _)) => dir,
|
|
||||||
None => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors produced when resolving a [`PromptRef`].
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum LoaderError {
|
|
||||||
#[error("invalid prompt reference '{raw}': {reason}")]
|
|
||||||
InvalidRef { raw: String, reason: String },
|
|
||||||
#[error("unknown prompt prefix '{prefix}' in reference '{raw}'")]
|
|
||||||
UnknownPrefix { raw: String, prefix: String },
|
|
||||||
#[error(
|
|
||||||
"unqualified prompt reference '{raw}' requires a current prefix \
|
|
||||||
(include it from inside another template, or use an explicit \
|
|
||||||
$prefix/path form)"
|
|
||||||
)]
|
|
||||||
UnqualifiedWithoutCurrent { raw: String },
|
|
||||||
#[error("prompt prefix '{prefix}' is not configured for this loader")]
|
|
||||||
PrefixNotConfigured { prefix: &'static str },
|
|
||||||
#[error("prompt asset not found: '{}'", .reference.to_qualified_string())]
|
|
||||||
NotFound { reference: PromptRef },
|
|
||||||
#[error("failed to read prompt asset '{}': {source}", .reference.to_qualified_string())]
|
|
||||||
Io {
|
|
||||||
reference: PromptRef,
|
|
||||||
#[source]
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loader that resolves [`PromptRef`]s against the configured prompt
|
|
||||||
/// libraries. Cheap to clone.
|
|
||||||
///
|
|
||||||
/// Also carries the auto-discovered `prompts.toml` pack file paths so
|
|
||||||
/// [`crate::prompt::catalog::PromptCatalog`] can read the same user/workspace
|
|
||||||
/// layers without a separate plumbing channel. These fields do not
|
|
||||||
/// affect `$prefix` asset resolution — they are purely metadata
|
|
||||||
/// consulted by the catalog loader.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PromptLoader {
|
|
||||||
user_dir: Option<PathBuf>,
|
|
||||||
workspace_dir: Option<PathBuf>,
|
|
||||||
user_pack_file: Option<PathBuf>,
|
|
||||||
workspace_pack_file: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromptLoader {
|
|
||||||
/// Loader with only the builtin `$yoi` library available.
|
|
||||||
/// `$user` / `$workspace` references fail with
|
|
||||||
/// [`LoaderError::PrefixNotConfigured`].
|
|
||||||
pub fn builtins_only() -> Self {
|
|
||||||
Self {
|
|
||||||
user_dir: None,
|
|
||||||
workspace_dir: None,
|
|
||||||
user_pack_file: None,
|
|
||||||
workspace_pack_file: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loader with optional user and workspace prompt directories.
|
|
||||||
pub fn new(user_dir: Option<PathBuf>, workspace_dir: Option<PathBuf>) -> Self {
|
|
||||||
Self {
|
|
||||||
user_dir,
|
|
||||||
workspace_dir,
|
|
||||||
user_pack_file: None,
|
|
||||||
workspace_pack_file: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override pack file paths supplied by the caller's profile/manifest
|
|
||||||
/// resolution context.
|
|
||||||
pub fn with_pack_files(
|
|
||||||
mut self,
|
|
||||||
user_pack_file: Option<PathBuf>,
|
|
||||||
workspace_pack_file: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
self.user_pack_file = user_pack_file;
|
|
||||||
self.workspace_pack_file = workspace_pack_file;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root of the `$user` prompt library, if configured.
|
|
||||||
pub fn user_dir(&self) -> Option<&Path> {
|
|
||||||
self.user_dir.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root of the `$workspace` prompt library, if configured.
|
|
||||||
pub fn workspace_dir(&self) -> Option<&Path> {
|
|
||||||
self.workspace_dir.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Auto-discovered path to the user-layer `prompts.toml` pack, if any.
|
|
||||||
pub fn user_pack_file(&self) -> Option<&Path> {
|
|
||||||
self.user_pack_file.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Auto-discovered path to the workspace-layer `prompts.toml` pack, if any.
|
|
||||||
pub fn workspace_pack_file(&self) -> Option<&Path> {
|
|
||||||
self.workspace_pack_file.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a string reference into a [`PromptRef`]. Unqualified
|
|
||||||
/// references (no leading `$prefix/`) are resolved against
|
|
||||||
/// `current`: the prefix is inherited, and the path is joined to
|
|
||||||
/// the current ref's directory.
|
|
||||||
pub fn parse_ref(
|
|
||||||
&self,
|
|
||||||
raw: &str,
|
|
||||||
current: Option<&PromptRef>,
|
|
||||||
) -> Result<PromptRef, LoaderError> {
|
|
||||||
let trimmed = raw.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Err(LoaderError::InvalidRef {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
reason: "reference must not be empty".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(prefix) = trimmed.strip_prefix('$') {
|
|
||||||
let (prefix_name, rest) =
|
|
||||||
prefix
|
|
||||||
.split_once('/')
|
|
||||||
.ok_or_else(|| LoaderError::InvalidRef {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
reason: "prefix must be followed by '/'".into(),
|
|
||||||
})?;
|
|
||||||
let prefix = parse_prefix(raw, prefix_name)?;
|
|
||||||
let path = normalize_path(raw, rest)?;
|
|
||||||
Ok(PromptRef { prefix, path })
|
|
||||||
} else {
|
|
||||||
let Some(current) = current else {
|
|
||||||
return Err(LoaderError::UnqualifiedWithoutCurrent {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let dir = current.dir();
|
|
||||||
let joined = if dir.is_empty() {
|
|
||||||
trimmed.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{dir}/{trimmed}")
|
|
||||||
};
|
|
||||||
let path = normalize_path(raw, &joined)?;
|
|
||||||
Ok(PromptRef {
|
|
||||||
prefix: current.prefix,
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a [`PromptRef`] to its raw template source. Hard-errors
|
|
||||||
/// when the prefix is not configured or the file does not exist.
|
|
||||||
pub fn load(&self, reference: &PromptRef) -> Result<String, LoaderError> {
|
|
||||||
match reference.prefix {
|
|
||||||
Prefix::Yoi => load_from_include_dir(&BUILTIN_PROMPTS, reference),
|
|
||||||
Prefix::User => match self.user_dir.as_deref() {
|
|
||||||
Some(dir) => load_from_dir(dir, reference),
|
|
||||||
None => Err(LoaderError::PrefixNotConfigured {
|
|
||||||
prefix: PREFIX_USER,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Prefix::Workspace => match self.workspace_dir.as_deref() {
|
|
||||||
Some(dir) => load_from_dir(dir, reference),
|
|
||||||
None => Err(LoaderError::PrefixNotConfigured {
|
|
||||||
prefix: PREFIX_WORKSPACE,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse `raw` against `current`, then load the resulting ref.
|
|
||||||
/// Convenience wrapper for the minijinja loader hook.
|
|
||||||
pub fn resolve(
|
|
||||||
&self,
|
|
||||||
raw: &str,
|
|
||||||
current: Option<&PromptRef>,
|
|
||||||
) -> Result<(PromptRef, String), LoaderError> {
|
|
||||||
let reference = self.parse_ref(raw, current)?;
|
|
||||||
let source = self.load(&reference)?;
|
|
||||||
Ok((reference, source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_prefix(raw: &str, prefix_name: &str) -> Result<Prefix, LoaderError> {
|
|
||||||
match prefix_name {
|
|
||||||
"yoi" => Ok(Prefix::Yoi),
|
|
||||||
"user" => Ok(Prefix::User),
|
|
||||||
"workspace" => Ok(Prefix::Workspace),
|
|
||||||
_ => Err(LoaderError::UnknownPrefix {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
prefix: format!("${prefix_name}"),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_path(raw: &str, rest: &str) -> Result<String, LoaderError> {
|
|
||||||
let cleaned = rest.trim_matches('/').trim();
|
|
||||||
if cleaned.is_empty() {
|
|
||||||
return Err(LoaderError::InvalidRef {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
reason: "path component must not be empty".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if cleaned.split('/').any(|seg| seg == "." || seg == "..") {
|
|
||||||
return Err(LoaderError::InvalidRef {
|
|
||||||
raw: raw.to_string(),
|
|
||||||
reason: "path must not contain '.' or '..' segments".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(cleaned.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_from_dir(dir: &Path, reference: &PromptRef) -> Result<String, LoaderError> {
|
|
||||||
let path = dir.join(format!("{}.md", reference.path));
|
|
||||||
match std::fs::read_to_string(&path) {
|
|
||||||
Ok(s) => Ok(s),
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(LoaderError::NotFound {
|
|
||||||
reference: reference.clone(),
|
|
||||||
}),
|
|
||||||
Err(source) => Err(LoaderError::Io {
|
|
||||||
reference: reference.clone(),
|
|
||||||
source,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_from_include_dir(dir: &Dir<'static>, reference: &PromptRef) -> Result<String, LoaderError> {
|
|
||||||
let path = format!("{}.md", reference.path);
|
|
||||||
dir.get_file(&path)
|
|
||||||
.and_then(|f| f.contents_utf8())
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.ok_or_else(|| LoaderError::NotFound {
|
|
||||||
reference: reference.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builtin_default_resolves() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let (r, source) = loader.resolve("$yoi/default", None).unwrap();
|
|
||||||
assert_eq!(r.to_qualified_string(), "$yoi/default");
|
|
||||||
assert!(!source.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builtin_ticket_role_instructions_resolve() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
for role in ["intake", "orchestrator", "coder", "reviewer"] {
|
|
||||||
let (reference, source) = loader.resolve(&format!("$yoi/role/{role}"), None).unwrap();
|
|
||||||
assert_eq!(reference.to_qualified_string(), format!("$yoi/role/{role}"));
|
|
||||||
assert!(source.contains("first committed user message"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builtin_subdirectory_lookup() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let (_, source) = loader.resolve("$yoi/common/tool-usage", None).unwrap();
|
|
||||||
assert!(source.contains("tool"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_prefix_resolves() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
let user_dir = tmp.path().to_path_buf();
|
|
||||||
std::fs::write(user_dir.join("my.md"), "user-body").unwrap();
|
|
||||||
let loader = PromptLoader::new(Some(user_dir), None);
|
|
||||||
let (_, source) = loader.resolve("$user/my", None).unwrap();
|
|
||||||
assert_eq!(source, "user-body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn workspace_prefix_resolves() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
let ws_dir = tmp.path().to_path_buf();
|
|
||||||
std::fs::write(ws_dir.join("custom.md"), "ws-body").unwrap();
|
|
||||||
let loader = PromptLoader::new(None, Some(ws_dir));
|
|
||||||
let (_, source) = loader.resolve("$workspace/custom", None).unwrap();
|
|
||||||
assert_eq!(source, "ws-body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_file_is_hard_error() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = loader.resolve("$yoi/definitely-missing", None).unwrap_err();
|
|
||||||
assert!(matches!(err, LoaderError::NotFound { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_prefix_not_configured_errors() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = loader.resolve("$user/my", None).unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
LoaderError::PrefixNotConfigured { prefix: "$user" }
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_prefix_errors() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = loader.resolve("$bogus/x", None).unwrap_err();
|
|
||||||
assert!(matches!(err, LoaderError::UnknownPrefix { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unqualified_ref_without_current_errors() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = loader.resolve("default", None).unwrap_err();
|
|
||||||
assert!(matches!(err, LoaderError::UnqualifiedWithoutCurrent { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unqualified_ref_resolves_relative_to_current() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let current = loader.parse_ref("$yoi/common/tool-usage", None).unwrap();
|
|
||||||
// Sibling lookup under the same prefix and directory.
|
|
||||||
let sibling = loader.parse_ref("workspace", Some(¤t)).unwrap();
|
|
||||||
assert_eq!(sibling.to_qualified_string(), "$yoi/common/workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unqualified_ref_from_root_file_has_empty_dir() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let current = loader.parse_ref("$yoi/default", None).unwrap();
|
|
||||||
let sibling = loader.parse_ref("other", Some(¤t)).unwrap();
|
|
||||||
assert_eq!(sibling.to_qualified_string(), "$yoi/other");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_prefix_overrides_current() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
let user_dir = tmp.path().to_path_buf();
|
|
||||||
std::fs::write(user_dir.join("custom.md"), "user-body").unwrap();
|
|
||||||
let loader = PromptLoader::new(Some(user_dir), None);
|
|
||||||
|
|
||||||
let current = loader.parse_ref("$yoi/default", None).unwrap();
|
|
||||||
// Even with an $yoi-rooted current, an explicit $user
|
|
||||||
// prefix must win.
|
|
||||||
let (reference, source) = loader.resolve("$user/custom", Some(¤t)).unwrap();
|
|
||||||
assert_eq!(reference.to_qualified_string(), "$user/custom");
|
|
||||||
assert_eq!(source, "user-body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn traversal_segments_rejected() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = loader.resolve("$yoi/../etc/passwd", None).unwrap_err();
|
|
||||||
assert!(matches!(err, LoaderError::InvalidRef { .. }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pub(crate) mod agents_md;
|
pub(crate) mod agents_md;
|
||||||
pub(crate) mod catalog;
|
pub(crate) mod catalog;
|
||||||
pub(crate) mod loader;
|
pub(crate) mod source;
|
||||||
pub(crate) mod system;
|
pub(crate) mod system;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! Immutable effective Prompt catalog carrier.
|
||||||
|
//!
|
||||||
|
//! Prompt sources are resolved and evaluated by Workspace config authority.
|
||||||
|
//! This type carries only the already-materialized projection into Worker
|
||||||
|
//! construction; it performs no filesystem, prefix, relative-path, user, or
|
||||||
|
//! repository discovery.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::catalog::EffectivePromptCatalog;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PromptCatalogSource {
|
||||||
|
effective_catalog: Option<Arc<EffectivePromptCatalog>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptCatalogSource {
|
||||||
|
pub fn builtins_only() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_effective_catalog(mut self, catalog: EffectivePromptCatalog) -> Self {
|
||||||
|
self.effective_catalog = Some(Arc::new(catalog));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn effective_catalog(&self) -> Option<&EffectivePromptCatalog> {
|
||||||
|
self.effective_catalog.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
-539
@@ -3,7 +3,7 @@
|
|||||||
//! Manifests describe the system prompt body as a reference to a
|
//! Manifests describe the system prompt body as a reference to a
|
||||||
//! prompt asset (`worker.instruction`, see [`manifest::EngineManifest`]).
|
//! prompt asset (`worker.instruction`, see [`manifest::EngineManifest`]).
|
||||||
//! [`SystemPromptTemplate`] resolves that reference through a
|
//! [`SystemPromptTemplate`] resolves that reference through a
|
||||||
//! [`PromptLoader`], parses the source as a minijinja template, and
|
//! [`PromptCatalogSource`], parses the source as a minijinja template, and
|
||||||
//! eagerly syntax-checks it at Worker construction. The final system
|
//! eagerly syntax-checks it at Worker construction. The final system
|
||||||
//! prompt is materialised exactly once just before the first LLM turn:
|
//! prompt is materialised exactly once just before the first LLM turn:
|
||||||
//! the rendered body is appended with a fixed trailing section carrying
|
//! the rendered body is appended with a fixed trailing section carrying
|
||||||
@@ -22,17 +22,16 @@ use std::sync::Arc;
|
|||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, SecondsFormat, Utc};
|
||||||
use manifest::Scope;
|
use manifest::Scope;
|
||||||
use minijinja::value::Value;
|
use minijinja::value::Value;
|
||||||
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::feature::{FeatureInstructionDeclaration, dedupe_instruction_contributions};
|
use crate::feature::{FeatureInstructionDeclaration, dedupe_instruction_contributions};
|
||||||
use crate::prompt::catalog::{CatalogError, PromptCatalog};
|
use crate::prompt::catalog::{CatalogError, PromptCatalog};
|
||||||
use crate::prompt::loader::{LoaderError, PromptLoader, PromptRef};
|
#[cfg(test)]
|
||||||
|
use crate::prompt::catalog::{EffectivePromptCatalog, builtin_prompt_templates};
|
||||||
|
use crate::prompt::source::PromptCatalogSource;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SystemPromptError {
|
pub enum SystemPromptError {
|
||||||
#[error("failed to resolve instruction reference: {0}")]
|
|
||||||
LoaderResolve(#[source] LoaderError),
|
|
||||||
#[error("system prompt template parse error: {0}")]
|
#[error("system prompt template parse error: {0}")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
#[error("system prompt template render error: {0}")]
|
#[error("system prompt template render error: {0}")]
|
||||||
@@ -41,69 +40,37 @@ pub enum SystemPromptError {
|
|||||||
Catalog(#[from] CatalogError),
|
Catalog(#[from] CatalogError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed instruction template bound to a prompt loader.
|
/// Parsed instruction template bound to one immutable effective Prompt catalog.
|
||||||
///
|
|
||||||
/// Holds a minijinja Environment pre-populated with the instruction
|
|
||||||
/// template registered under its fully-qualified name (`$prefix/path`).
|
|
||||||
/// Includes are resolved via the loader using a path-join callback that
|
|
||||||
/// tracks the including template's prefix and directory, so
|
|
||||||
/// `{% include "sibling" %}` fragments work as expected.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SystemPromptTemplate {
|
pub struct SystemPromptTemplate {
|
||||||
env: Arc<Environment<'static>>,
|
catalog: Arc<PromptCatalog>,
|
||||||
instruction_name: String,
|
instruction_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemPromptTemplate {
|
impl SystemPromptTemplate {
|
||||||
/// Parse the instruction asset referenced by `instruction_ref`
|
/// Resolve an exact catalog-root dotted Prompt name and eagerly verify it.
|
||||||
/// using the supplied [`PromptLoader`]. The reference is resolved
|
pub fn parse(
|
||||||
/// at parse time so syntax errors surface immediately.
|
instruction_ref: &str,
|
||||||
pub fn parse(instruction_ref: &str, loader: PromptLoader) -> Result<Self, SystemPromptError> {
|
loader: PromptCatalogSource,
|
||||||
let root_ref = loader
|
) -> Result<Self, SystemPromptError> {
|
||||||
.parse_ref(instruction_ref, None)
|
let instruction_name = exact_prompt_name(instruction_ref).ok_or_else(|| {
|
||||||
.map_err(SystemPromptError::LoaderResolve)?;
|
SystemPromptError::Parse(format!(
|
||||||
let source = loader
|
"instruction must be an exact catalog-root dotted Prompt name: {instruction_ref}"
|
||||||
.load(&root_ref)
|
))
|
||||||
.map_err(SystemPromptError::LoaderResolve)?;
|
})?;
|
||||||
let root_name = root_ref.to_qualified_string();
|
let catalog = if let Some(projection) = loader.effective_catalog() {
|
||||||
|
Arc::new(PromptCatalog::from_projection(projection.clone())?)
|
||||||
let mut env = Environment::new();
|
} else {
|
||||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
PromptCatalog::builtins_only()?
|
||||||
|
};
|
||||||
// Path-join callback: compute the target template name when a
|
if !catalog.contains(&instruction_name) {
|
||||||
// template includes another by a possibly-unqualified string.
|
return Err(SystemPromptError::Parse(format!(
|
||||||
// The joined name is then looked up via `set_loader` below.
|
"Prompt '{instruction_name}' is not present in the effective catalog"
|
||||||
let loader_for_join = loader.clone();
|
)));
|
||||||
env.set_path_join_callback(move |name, parent| {
|
}
|
||||||
let parent_ref = loader_for_join.parse_ref(parent, None).ok();
|
|
||||||
match loader_for_join.parse_ref(name, parent_ref.as_ref()) {
|
|
||||||
Ok(r) => r.to_qualified_string().into(),
|
|
||||||
// Propagate the raw name on error so set_loader surfaces
|
|
||||||
// a proper TemplateNotFound/LoaderError to the caller.
|
|
||||||
Err(_) => name.to_string().into(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let loader_for_src = loader.clone();
|
|
||||||
env.set_loader(move |name| {
|
|
||||||
let reference = loader_for_src
|
|
||||||
.parse_ref(name, None)
|
|
||||||
.map_err(|e| minijinja::Error::new(ErrorKind::TemplateNotFound, e.to_string()))?;
|
|
||||||
match loader_for_src.load(&reference) {
|
|
||||||
Ok(source) => Ok(Some(source)),
|
|
||||||
Err(e) => Err(minijinja::Error::new(
|
|
||||||
ErrorKind::TemplateNotFound,
|
|
||||||
e.to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
env.add_template_owned(root_name.clone(), source)
|
|
||||||
.map_err(|e| SystemPromptError::Parse(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
env: Arc::new(env),
|
catalog,
|
||||||
instruction_name: root_name,
|
instruction_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,18 +79,14 @@ impl SystemPromptTemplate {
|
|||||||
/// section is assembled in Rust so that authored templates cannot
|
/// section is assembled in Rust so that authored templates cannot
|
||||||
/// accidentally omit the scope boundary or the project instructions.
|
/// accidentally omit the scope boundary or the project instructions.
|
||||||
pub fn render(&self, ctx: &SystemPromptContext<'_>) -> Result<String, SystemPromptError> {
|
pub fn render(&self, ctx: &SystemPromptContext<'_>) -> Result<String, SystemPromptError> {
|
||||||
let tmpl = self
|
let body = self
|
||||||
.env
|
.catalog
|
||||||
.get_template(&self.instruction_name)
|
.render_name(&self.instruction_name, ctx.to_minijinja_value())
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
.map_err(|error| SystemPromptError::Render(error.to_string()))?;
|
||||||
let body = tmpl
|
|
||||||
.render(ctx.to_minijinja_value())
|
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
|
||||||
append_trailing_section(
|
append_trailing_section(
|
||||||
&body,
|
&body,
|
||||||
&self.env,
|
|
||||||
ctx,
|
ctx,
|
||||||
ctx.prompts,
|
&self.catalog,
|
||||||
ctx.scope,
|
ctx.scope,
|
||||||
ctx.agents_md.as_deref(),
|
ctx.agents_md.as_deref(),
|
||||||
ctx.resident_summary,
|
ctx.resident_summary,
|
||||||
@@ -273,6 +236,22 @@ impl ToolCapabilities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exact_prompt_name(reference: &str) -> Option<String> {
|
||||||
|
let candidate = reference.to_string();
|
||||||
|
if candidate.is_empty()
|
||||||
|
|| candidate.split('.').any(|segment| {
|
||||||
|
segment.is_empty()
|
||||||
|
|| !segment.chars().all(|character| {
|
||||||
|
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the final system prompt by appending the fixed trailing
|
/// Build the final system prompt by appending the fixed trailing
|
||||||
/// section to `body`. The Rust side owns the layout (blank-line
|
/// section to `body`. The Rust side owns the layout (blank-line
|
||||||
/// separators, trailing-whitespace trim); each section's header + body
|
/// separators, trailing-whitespace trim); each section's header + body
|
||||||
@@ -281,7 +260,6 @@ impl ToolCapabilities {
|
|||||||
/// per-pack without touching this function.
|
/// per-pack without touching this function.
|
||||||
fn append_trailing_section(
|
fn append_trailing_section(
|
||||||
body: &str,
|
body: &str,
|
||||||
env: &Environment<'static>,
|
|
||||||
ctx: &SystemPromptContext<'_>,
|
ctx: &SystemPromptContext<'_>,
|
||||||
prompts: &PromptCatalog,
|
prompts: &PromptCatalog,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
@@ -315,12 +293,15 @@ fn append_trailing_section(
|
|||||||
}
|
}
|
||||||
for instruction in dedupe_instruction_contributions(ctx.feature_instructions.iter().cloned()) {
|
for instruction in dedupe_instruction_contributions(ctx.feature_instructions.iter().cloned()) {
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
let template = env
|
let prompt_ref = exact_prompt_name(&instruction.prompt_ref).ok_or_else(|| {
|
||||||
.get_template(&instruction.prompt_ref)
|
SystemPromptError::Render(format!(
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
"feature instruction must be an exact catalog-root dotted Prompt name: {}",
|
||||||
let section = template
|
instruction.prompt_ref
|
||||||
.render(ctx.to_minijinja_value())
|
))
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
})?;
|
||||||
|
let section = prompts
|
||||||
|
.render_name(&prompt_ref, ctx.to_minijinja_value())
|
||||||
|
.map_err(|error| SystemPromptError::Render(error.to_string()))?;
|
||||||
let section = section.trim_end_matches(&['\n', ' '][..]);
|
let section = section.trim_end_matches(&['\n', ' '][..]);
|
||||||
if !section.trim().is_empty() {
|
if !section.trim().is_empty() {
|
||||||
out.push_str(section);
|
out.push_str(section);
|
||||||
@@ -335,13 +316,6 @@ fn append_trailing_section(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bridge used by [`Worker::ensure_system_prompt_materialized`] so tests
|
|
||||||
/// can construct a synthetic context without going through a full Worker.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn __instruction_ref_for_tests(raw: &str, loader: &PromptLoader) -> Option<PromptRef> {
|
|
||||||
loader.parse_ref(raw, None).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -354,487 +328,91 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_scope(dir: &Path) -> Scope {
|
fn build_scope(dir: &Path) -> Scope {
|
||||||
let cfg = ScopeConfig {
|
Scope::from_config(&ScopeConfig {
|
||||||
allow: vec![ScopeRule {
|
allow: vec![ScopeRule {
|
||||||
target: dir.to_path_buf(),
|
target: dir.to_path_buf(),
|
||||||
permission: Permission::Write,
|
permission: Permission::Write,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
}],
|
}],
|
||||||
deny: Vec::new(),
|
deny: Vec::new(),
|
||||||
};
|
})
|
||||||
Scope::from_config(&cfg).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ctx<'a>(
|
|
||||||
cwd: &'a Path,
|
|
||||||
scope: &'a Scope,
|
|
||||||
tools: Vec<String>,
|
|
||||||
agents_md: Option<String>,
|
|
||||||
) -> SystemPromptContext<'a> {
|
|
||||||
SystemPromptContext {
|
|
||||||
now: fixed_now(),
|
|
||||||
cwd: cwd.display().to_string().into(),
|
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
|
||||||
scope,
|
|
||||||
tool_names: tools,
|
|
||||||
feature_instructions: &[],
|
|
||||||
agents_md,
|
|
||||||
resident_summary: None,
|
|
||||||
prompts: test_prompts(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ctx_with_summary<'a>(
|
|
||||||
cwd: &'a Path,
|
|
||||||
scope: &'a Scope,
|
|
||||||
summary: Option<&'a str>,
|
|
||||||
) -> SystemPromptContext<'a> {
|
|
||||||
SystemPromptContext {
|
|
||||||
now: fixed_now(),
|
|
||||||
cwd: cwd.display().to_string().into(),
|
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
|
||||||
scope,
|
|
||||||
tool_names: Vec::new(),
|
|
||||||
feature_instructions: &[],
|
|
||||||
agents_md: None,
|
|
||||||
resident_summary: summary,
|
|
||||||
prompts: test_prompts(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn memory_tool_names() -> Vec<String> {
|
|
||||||
["MemoryQuery", "MemoryReadDocument", "MemoryUpdateDocument"]
|
|
||||||
.into_iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ticket_instruction() -> FeatureInstructionDeclaration {
|
|
||||||
FeatureInstructionDeclaration::new(
|
|
||||||
crate::feature::FeatureInstructionId::builtin("ticket.workflow"),
|
|
||||||
"$yoi/common/tickets",
|
|
||||||
"Ticket workflow guidance",
|
|
||||||
)
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sub_worker_orchestration_instruction() -> FeatureInstructionDeclaration {
|
fn context<'a>(
|
||||||
FeatureInstructionDeclaration::new(
|
cwd: &'a Path,
|
||||||
crate::feature::FeatureInstructionId::builtin("worker.orchestration"),
|
scope: &'a Scope,
|
||||||
"$yoi/common/worker-orchestration",
|
prompts: &'a PromptCatalog,
|
||||||
"Worker orchestration guidance",
|
) -> SystemPromptContext<'a> {
|
||||||
)
|
SystemPromptContext {
|
||||||
.unwrap()
|
now: fixed_now(),
|
||||||
|
cwd: cwd.to_string_lossy(),
|
||||||
|
tool_names: vec!["Read".into(), "Write".into()],
|
||||||
|
scope,
|
||||||
|
agents_md: Some("PROJECT RULES".into()),
|
||||||
|
resident_summary: Some("DURABLE MEMORY"),
|
||||||
|
language: "Japanese",
|
||||||
|
feature_instructions: &[],
|
||||||
|
prompts,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lazily-initialised builtin catalog shared across system-prompt
|
#[test]
|
||||||
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
|
fn exact_catalog_name_renders_once_with_trailing_sections() {
|
||||||
/// reference without forcing test bodies to create one per call.
|
|
||||||
fn test_prompts() -> &'static PromptCatalog {
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
static CELL: OnceLock<Arc<PromptCatalog>> = OnceLock::new();
|
|
||||||
CELL.get_or_init(|| PromptCatalog::builtins_only().unwrap())
|
|
||||||
.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_loader_with(file_name: &str, body: &str) -> (TempDir, PromptLoader) {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
std::fs::write(tmp.path().join(file_name), body).unwrap();
|
let scope = build_scope(tmp.path());
|
||||||
let loader = PromptLoader::new(Some(tmp.path().to_path_buf()), None);
|
let prompts = PromptCatalog::builtins_only().unwrap();
|
||||||
(tmp, loader)
|
let template =
|
||||||
}
|
SystemPromptTemplate::parse("default", PromptCatalogSource::builtins_only()).unwrap();
|
||||||
|
let rendered = template
|
||||||
#[test]
|
.render(&context(tmp.path(), &scope, &prompts))
|
||||||
fn instruction_default_resolves_to_yoi_default() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(dir.path(), &scope, memory_tool_names(), None))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Builtin default body must expose the tool and language policies.
|
assert!(rendered.contains("2026-08-14") || rendered.contains("2026-04-15"));
|
||||||
assert!(rendered.contains("### Memory"));
|
|
||||||
assert!(rendered.contains("small targeted `MemoryQuery`"));
|
|
||||||
assert!(rendered.contains("Strong lookup triggers include"));
|
|
||||||
assert!(rendered.contains("MemoryReadDocument"));
|
|
||||||
assert!(rendered.contains("Do not query memory every turn"));
|
|
||||||
assert!(rendered.contains("MemoryUpdateDocument"));
|
|
||||||
assert!(rendered.contains("## Language"));
|
|
||||||
assert!(rendered.contains("`language`: `match the user's language"));
|
|
||||||
// Trailing section must be present.
|
|
||||||
assert!(rendered.contains("## Working boundaries"));
|
assert!(rendered.contains("## Working boundaries"));
|
||||||
assert!(rendered.contains("Readable:"));
|
assert!(rendered.contains("PROJECT RULES"));
|
||||||
|
assert!(rendered.contains("DURABLE MEMORY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instruction_default_omits_memory_guidance_without_memory_tools() {
|
fn workspace_override_is_visible_through_builtin_static_include() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let mut templates = builtin_prompt_templates().unwrap();
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
templates.insert("common.workspace".into(), "WORKSPACE OVERRIDE".into());
|
||||||
let dir = TempDir::new().unwrap();
|
let projection = EffectivePromptCatalog::new(templates, 9, "schema", "toolchain").unwrap();
|
||||||
let scope = build_scope(dir.path());
|
let loader =
|
||||||
let rendered = tmpl
|
PromptCatalogSource::builtins_only().with_effective_catalog(projection.clone());
|
||||||
.render(&ctx(
|
let prompts = PromptCatalog::from_projection(projection).unwrap();
|
||||||
dir.path(),
|
let template = SystemPromptTemplate::parse("default", loader).unwrap();
|
||||||
&scope,
|
let tmp = TempDir::new().unwrap();
|
||||||
vec!["Read".into(), "Edit".into()],
|
let scope = build_scope(tmp.path());
|
||||||
None,
|
let rendered = template
|
||||||
))
|
.render(&context(tmp.path(), &scope, &prompts))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert!(rendered.contains("WORKSPACE OVERRIDE"));
|
||||||
assert!(!rendered.contains("### Memory"));
|
|
||||||
assert!(!rendered.contains("MemoryQuery"));
|
|
||||||
assert!(!rendered.contains("MemoryRead"));
|
|
||||||
assert!(!rendered.contains("MemoryWrite"));
|
|
||||||
assert!(!rendered.contains("MemoryEdit"));
|
|
||||||
assert!(!rendered.contains("MemoryDelete"));
|
|
||||||
assert!(rendered.contains("## Language"));
|
|
||||||
assert!(rendered.contains("## Working boundaries"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_guidance_is_included_for_ticket_feature_instruction() {
|
fn rejects_legacy_prefix_relative_and_missing_names() {
|
||||||
let loader = PromptLoader::builtins_only();
|
for reference in ["legacy/custom", "custom.md", "../custom", "missing"] {
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let instructions = [ticket_instruction()];
|
|
||||||
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
|
||||||
ctx.feature_instructions = &instructions;
|
|
||||||
let rendered = tmpl.render(&ctx).unwrap();
|
|
||||||
|
|
||||||
assert!(rendered.contains("## Ticket workflow"));
|
|
||||||
assert!(rendered.contains("available typed Ticket tools as the authority"));
|
|
||||||
assert!(rendered.contains("Do not invoke a Ticket CLI"));
|
|
||||||
assert!(rendered.contains("Distinguish implementation completion"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn feature_instruction_is_appended_even_when_template_does_not_include_it() {
|
|
||||||
let (_tmp, loader) = user_loader_with("minimal.md", "BASE ONLY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/minimal", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let instructions = [ticket_instruction()];
|
|
||||||
let mut ctx = ctx(dir.path(), &scope, vec![], None);
|
|
||||||
ctx.feature_instructions = &instructions;
|
|
||||||
let rendered = tmpl.render(&ctx).unwrap();
|
|
||||||
|
|
||||||
assert!(rendered.starts_with("BASE ONLY"));
|
|
||||||
assert!(rendered.contains("## Ticket workflow"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ticket_guidance_is_omitted_without_ticket_tools() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
vec!["Read".into(), "Edit".into()],
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!rendered.contains("## Ticket workflow"));
|
|
||||||
assert!(!rendered.contains("Do not invoke a Ticket CLI"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ticket_role_instructions_include_feature_ticket_guidance() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let instructions = [ticket_instruction()];
|
|
||||||
|
|
||||||
for role in ["intake", "orchestrator", "coder", "reviewer"] {
|
|
||||||
let tmpl =
|
|
||||||
SystemPromptTemplate::parse(&format!("$yoi/role/{role}"), loader.clone()).unwrap();
|
|
||||||
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
|
||||||
ctx.feature_instructions = &instructions;
|
|
||||||
let rendered = tmpl.render(&ctx).unwrap();
|
|
||||||
|
|
||||||
assert!(rendered.contains("## Ticket workflow"), "role: {role}");
|
|
||||||
assert!(
|
assert!(
|
||||||
rendered.contains("Do not invoke a Ticket CLI"),
|
SystemPromptTemplate::parse(reference, PromptCatalogSource::builtins_only())
|
||||||
"role: {role}"
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn memory_guidance_names_only_available_memory_tools() {
|
fn role_templates_are_selected_without_filesystem_resolution() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptCatalogSource::builtins_only();
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
for role in [
|
||||||
let dir = TempDir::new().unwrap();
|
"role.coder",
|
||||||
let scope = build_scope(dir.path());
|
"role.intake",
|
||||||
let rendered = tmpl
|
"role.orchestrator",
|
||||||
.render(&ctx(
|
"role.reviewer",
|
||||||
dir.path(),
|
] {
|
||||||
&scope,
|
assert!(
|
||||||
vec!["MemoryQuery".into(), "MemoryReadDocument".into()],
|
SystemPromptTemplate::parse(role, loader.clone()).is_ok(),
|
||||||
None,
|
"{role}"
|
||||||
))
|
);
|
||||||
.unwrap();
|
}
|
||||||
|
|
||||||
assert!(rendered.contains("### Memory"));
|
|
||||||
assert!(rendered.contains("small targeted `MemoryQuery`"));
|
|
||||||
assert!(rendered.contains("MemoryReadDocument"));
|
|
||||||
assert!(!rendered.contains("MemoryUpdateDocument"));
|
|
||||||
assert!(!rendered.contains("MemoryEdit"));
|
|
||||||
assert!(!rendered.contains("MemoryDelete"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn worker_orchestration_guidance_is_included_for_feature_instruction() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let instructions = [sub_worker_orchestration_instruction()];
|
|
||||||
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
|
||||||
ctx.feature_instructions = &instructions;
|
|
||||||
let rendered = tmpl.render(&ctx).unwrap();
|
|
||||||
|
|
||||||
assert!(rendered.contains("## SubWorker orchestration"));
|
|
||||||
assert!(rendered.contains("SubWorker notifications are background signals"));
|
|
||||||
assert!(rendered.contains("does not need to keep a turn open"));
|
|
||||||
assert!(rendered.contains("Do not use `sleep` or polling loops"));
|
|
||||||
assert!(rendered.contains("worktree state, diff, and test results"));
|
|
||||||
assert!(rendered.contains("not scheduler or auto-maintain authorization"));
|
|
||||||
assert!(rendered.contains("bypass user/Ticket authorization"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn worker_orchestration_guidance_is_omitted_without_sub_worker_management_tools() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
vec!["Read".into(), "Edit".into(), "MemoryReadDocument".into()],
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!rendered.contains("## Worker orchestration"));
|
|
||||||
assert!(!rendered.contains("spawned Worker notifications are background signals"));
|
|
||||||
assert!(!rendered.contains("does not need to keep a turn open"));
|
|
||||||
assert!(!rendered.contains("Do not use `sleep` or polling loops"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn instruction_prefix_addressing_user() {
|
|
||||||
let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/greet", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(rendered.starts_with("HELLO from"));
|
|
||||||
assert!(rendered.contains("## Working boundaries"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn instruction_prefix_addressing_workspace() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
std::fs::write(tmp.path().join("ws.md"), "WS {{ date }}").unwrap();
|
|
||||||
let loader = PromptLoader::new(None, Some(tmp.path().to_path_buf()));
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$workspace/ws", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(rendered.starts_with("WS 2026-04-15"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn include_unqualified_resolves_relative_to_current_prefix() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
// parent.md and sibling.md both under the user root.
|
|
||||||
std::fs::write(
|
|
||||||
tmp.path().join("parent.md"),
|
|
||||||
"PARENT\n{% include \"sibling\" %}",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
std::fs::write(tmp.path().join("sibling.md"), "SIBLING-BODY").unwrap();
|
|
||||||
let loader = PromptLoader::new(Some(tmp.path().to_path_buf()), None);
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/parent", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(rendered.contains("PARENT"));
|
|
||||||
assert!(rendered.contains("SIBLING-BODY"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn include_unqualified_from_subdirectory_resolves_in_same_dir() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
std::fs::create_dir(tmp.path().join("common")).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
tmp.path().join("common/header.md"),
|
|
||||||
"HEADER\n{% include \"nested\" %}",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
std::fs::write(tmp.path().join("common/nested.md"), "NESTED-OK").unwrap();
|
|
||||||
let loader = PromptLoader::new(Some(tmp.path().to_path_buf()), None);
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/common/header", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(rendered.contains("HEADER"));
|
|
||||||
assert!(rendered.contains("NESTED-OK"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn include_explicit_prefix_overrides_relative() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
tmp.path().join("root.md"),
|
|
||||||
"U-ROOT\n{% include \"$yoi/common/tool-usage\" %}",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let loader = PromptLoader::new(Some(tmp.path().to_path_buf()), None);
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/root", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
vec!["Read".into(), "Edit".into()],
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
assert!(rendered.contains("U-ROOT"));
|
|
||||||
// Pulled in from the builtin tool-usage asset.
|
|
||||||
assert!(rendered.contains("Read"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn prefix_with_missing_file_is_hard_error() {
|
|
||||||
let loader = PromptLoader::builtins_only();
|
|
||||||
let err = SystemPromptTemplate::parse("$yoi/definitely-missing", loader).unwrap_err();
|
|
||||||
assert!(matches!(err, SystemPromptError::LoaderResolve(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_fails_on_syntax_error() {
|
|
||||||
let (_tmp, loader) = user_loader_with("broken.md", "{{ unclosed");
|
|
||||||
let err = SystemPromptTemplate::parse("$user/broken", loader).unwrap_err();
|
|
||||||
assert!(matches!(err, SystemPromptError::Parse(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_fails_on_undefined_variable() {
|
|
||||||
let (_tmp, loader) = user_loader_with("ghost.md", "{{ ghost }}");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/ghost", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let err = tmpl
|
|
||||||
.render(&ctx(dir.path(), &scope, vec![], None))
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(err, SystemPromptError::Render(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_substitutes_date_cwd_tools() {
|
|
||||||
let (_tmp, loader) = user_loader_with(
|
|
||||||
"vars.md",
|
|
||||||
"date={{ date }} cwd={{ cwd }} tools={{ tools | join(',') }}",
|
|
||||||
);
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/vars", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
vec!["alpha".into(), "beta".into()],
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
assert!(rendered.contains("date=2026-04-15"));
|
|
||||||
assert!(rendered.contains(&format!("cwd={}", dir.path().display())));
|
|
||||||
assert!(rendered.contains("tools=alpha,beta"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trailing_section_always_contains_scope_summary() {
|
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(rendered.contains("## Working boundaries"));
|
|
||||||
assert!(rendered.contains("Readable:"));
|
|
||||||
assert!(rendered.contains("Writable:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trailing_section_contains_agents_md_when_present() {
|
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
vec![],
|
|
||||||
Some("PROJECT DOCS".into()),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
assert!(rendered.contains("## Project instructions (AGENTS.md)"));
|
|
||||||
assert!(rendered.contains("PROJECT DOCS"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trailing_section_omits_agents_md_when_absent() {
|
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
|
||||||
assert!(!rendered.contains("AGENTS.md"));
|
|
||||||
assert!(!rendered.contains("Project instructions"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trailing_section_renders_resident_summary_body() {
|
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx_with_summary(
|
|
||||||
dir.path(),
|
|
||||||
&scope,
|
|
||||||
Some("Persistent summary body"),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
assert!(rendered.contains("## Resident memory summary"));
|
|
||||||
assert!(rendered.contains("Persistent summary body"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trailing_section_omits_resident_summary_when_none_or_empty() {
|
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
|
||||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
|
||||||
let dir = TempDir::new().unwrap();
|
|
||||||
let scope = build_scope(dir.path());
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx_with_summary(dir.path(), &scope, None))
|
|
||||||
.unwrap();
|
|
||||||
assert!(!rendered.contains("Resident memory summary"));
|
|
||||||
|
|
||||||
let rendered = tmpl
|
|
||||||
.render(&ctx_with_summary(dir.path(), &scope, Some(" \n")))
|
|
||||||
.unwrap();
|
|
||||||
assert!(!rendered.contains("Resident memory summary"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use manifest::{
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::PromptLoader;
|
use crate::PromptCatalogSource;
|
||||||
use crate::controller::register_worker_tools;
|
use crate::controller::register_worker_tools;
|
||||||
use crate::internal_worker::{
|
use crate::internal_worker::{
|
||||||
EphemeralSessionStore, InternalWorkerSessionStatus, prepare_internal_worker_session,
|
EphemeralSessionStore, InternalWorkerSessionStatus, prepare_internal_worker_session,
|
||||||
@@ -44,7 +44,7 @@ struct SubWorkerSpawnInput {
|
|||||||
/// unambiguous profile slug. Raw/path selectors are rejected.
|
/// unambiguous profile slug. Raw/path selectors are rejected.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
/// Instruction-file reference (e.g. `$yoi/default`, `$user/my-agent`).
|
/// Exact catalog-root dotted Prompt name (for example `default` or `role.coder`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
instruction: Option<String>,
|
instruction: Option<String>,
|
||||||
/// Child process/tool working directory. This is not the runtime workspace
|
/// Child process/tool working directory. This is not the runtime workspace
|
||||||
@@ -276,7 +276,7 @@ pub struct SubWorkerSpawnTool {
|
|||||||
/// child config from reusable fields here, and selected profiles are
|
/// child config from reusable fields here, and selected profiles are
|
||||||
/// merged into the same internal handoff shape before launch.
|
/// merged into the same internal handoff shape before launch.
|
||||||
spawner_manifest: WorkerManifest,
|
spawner_manifest: WorkerManifest,
|
||||||
prompt_loader: PromptLoader,
|
prompt_loader: PromptCatalogSource,
|
||||||
/// Compact selector list shared by tool description and diagnostics.
|
/// Compact selector list shared by tool description and diagnostics.
|
||||||
available_profiles: AvailableProfiles,
|
available_profiles: AvailableProfiles,
|
||||||
/// Spawner's runtime scope. After a successful spawn, the
|
/// Spawner's runtime scope. After a successful spawn, the
|
||||||
@@ -310,7 +310,7 @@ impl SubWorkerSpawnTool {
|
|||||||
spawner_cwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
registry: Arc<SpawnedWorkerRegistry>,
|
registry: Arc<SpawnedWorkerRegistry>,
|
||||||
spawner_manifest: WorkerManifest,
|
spawner_manifest: WorkerManifest,
|
||||||
prompt_loader: PromptLoader,
|
prompt_loader: PromptCatalogSource,
|
||||||
available_profiles: AvailableProfiles,
|
available_profiles: AvailableProfiles,
|
||||||
spawner_scope: SharedScope,
|
spawner_scope: SharedScope,
|
||||||
delegation_scope: DelegationScope,
|
delegation_scope: DelegationScope,
|
||||||
@@ -827,7 +827,6 @@ fn build_spawn_config_json(
|
|||||||
let config = WorkerManifestConfig {
|
let config = WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: model.clone(),
|
model: model.clone(),
|
||||||
engine: EngineManifestConfig {
|
engine: EngineManifestConfig {
|
||||||
@@ -870,7 +869,6 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi
|
|||||||
WorkerManifestConfig {
|
WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some(manifest.worker.name.clone()),
|
name: Some(manifest.worker.name.clone()),
|
||||||
prompt_pack: manifest.worker.prompt_pack.clone(),
|
|
||||||
},
|
},
|
||||||
model: manifest.model.clone(),
|
model: manifest.model.clone(),
|
||||||
engine: EngineManifestConfig {
|
engine: EngineManifestConfig {
|
||||||
@@ -1010,7 +1008,7 @@ fn sub_worker_spawn_tool_impl(
|
|||||||
spawner_cwd.clone(),
|
spawner_cwd.clone(),
|
||||||
registry.clone(),
|
registry.clone(),
|
||||||
spawner_manifest.clone(),
|
spawner_manifest.clone(),
|
||||||
prompts.loader(),
|
prompts.source(),
|
||||||
available_profiles,
|
available_profiles,
|
||||||
spawner_scope.clone(),
|
spawner_scope.clone(),
|
||||||
DelegationScope::from_config(&spawner_manifest.delegation_scope)
|
DelegationScope::from_config(&spawner_manifest.delegation_scope)
|
||||||
@@ -1086,7 +1084,7 @@ model_id = "reviewer-model"
|
|||||||
kind = "none"
|
kind = "none"
|
||||||
|
|
||||||
[engine]
|
[engine]
|
||||||
instruction = "$yoi/reviewer"
|
instruction = "role.reviewer"
|
||||||
language = "Reviewerish"
|
language = "Reviewerish"
|
||||||
max_tokens = 3333
|
max_tokens = 3333
|
||||||
|
|
||||||
@@ -1136,14 +1134,7 @@ extract_threshold = 4000
|
|||||||
let observed_parent_write_revoked = Arc::new(AtomicBool::new(false));
|
let observed_parent_write_revoked = Arc::new(AtomicBool::new(false));
|
||||||
let observed_instruction_override = Arc::new(AtomicBool::new(false));
|
let observed_instruction_override = Arc::new(AtomicBool::new(false));
|
||||||
let fail_requests = Arc::new(AtomicBool::new(false));
|
let fail_requests = Arc::new(AtomicBool::new(false));
|
||||||
let workspace_prompts = runtime.path().join("workspace-prompts");
|
let prompt_loader = PromptCatalogSource::builtins_only();
|
||||||
std::fs::create_dir_all(&workspace_prompts).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
workspace_prompts.join("custom-reviewer.md"),
|
|
||||||
"WORKSPACE REVIEWER OVERRIDE",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let prompt_loader = PromptLoader::new(None, Some(workspace_prompts));
|
|
||||||
let (parent_method_tx, mut parent_method_rx) = mpsc::channel(8);
|
let (parent_method_tx, mut parent_method_rx) = mpsc::channel(8);
|
||||||
let tool = SubWorkerSpawnTool::new(
|
let tool = SubWorkerSpawnTool::new(
|
||||||
"parent".into(),
|
"parent".into(),
|
||||||
@@ -1170,7 +1161,7 @@ extract_threshold = 4000
|
|||||||
let input = serde_json::json!({
|
let input = serde_json::json!({
|
||||||
"name": "reviewer-child",
|
"name": "reviewer-child",
|
||||||
"profile": "project:reviewer",
|
"profile": "project:reviewer",
|
||||||
"instruction": "$workspace/custom-reviewer",
|
"instruction": "role.reviewer",
|
||||||
"task": "review immutable commit",
|
"task": "review immutable commit",
|
||||||
"scope": [{
|
"scope": [{
|
||||||
"target": workspace_root.clone(),
|
"target": workspace_root.clone(),
|
||||||
@@ -1468,7 +1459,7 @@ extract_threshold = 4000
|
|||||||
request
|
request
|
||||||
.system_prompt
|
.system_prompt
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|prompt| prompt.contains("WORKSPACE REVIEWER OVERRIDE")),
|
.is_some_and(|prompt| prompt.contains("review")),
|
||||||
Ordering::SeqCst,
|
Ordering::SeqCst,
|
||||||
);
|
);
|
||||||
if self.fail_requests.load(Ordering::SeqCst) {
|
if self.fail_requests.load(Ordering::SeqCst) {
|
||||||
@@ -1515,7 +1506,6 @@ extract_threshold = 4000
|
|||||||
WorkerManifestConfig {
|
WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
name: Some("parent".into()),
|
name: Some("parent".into()),
|
||||||
prompt_pack: None,
|
|
||||||
},
|
},
|
||||||
model: ModelManifest {
|
model: ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
@@ -1524,7 +1514,7 @@ extract_threshold = 4000
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
engine: EngineManifestConfig {
|
engine: EngineManifestConfig {
|
||||||
instruction: Some("$yoi/parent".into()),
|
instruction: Some("default".into()),
|
||||||
language: Some("Parentish".into()),
|
language: Some("Parentish".into()),
|
||||||
max_tokens: Some(1234),
|
max_tokens: Some(1234),
|
||||||
stop_sequences: Some(vec!["STOP".into()]),
|
stop_sequences: Some(vec!["STOP".into()]),
|
||||||
@@ -1604,7 +1594,7 @@ scheme = "anthropic"
|
|||||||
model_id = "coder-model"
|
model_id = "coder-model"
|
||||||
|
|
||||||
[engine]
|
[engine]
|
||||||
instruction = "$yoi/coder"
|
instruction = "role.coder"
|
||||||
language = "Coderish"
|
language = "Coderish"
|
||||||
max_tokens = 2222
|
max_tokens = 2222
|
||||||
"#;
|
"#;
|
||||||
@@ -1618,7 +1608,7 @@ scheme = "anthropic"
|
|||||||
model_id = "reviewer-model"
|
model_id = "reviewer-model"
|
||||||
|
|
||||||
[engine]
|
[engine]
|
||||||
instruction = "$yoi/reviewer"
|
instruction = "role.reviewer"
|
||||||
language = "Reviewerish"
|
language = "Reviewerish"
|
||||||
max_tokens = 3333
|
max_tokens = 3333
|
||||||
"#;
|
"#;
|
||||||
@@ -1635,8 +1625,7 @@ max_tokens = 3333
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_json =
|
let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
|
||||||
build_spawn_config_json("child", "$yoi/default", &[], &model, false).unwrap();
|
|
||||||
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
||||||
|
|
||||||
assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic));
|
assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic));
|
||||||
@@ -1658,8 +1647,7 @@ max_tokens = 3333
|
|||||||
ref_: Some("anthropic/claude-sonnet-4-6".into()),
|
ref_: Some("anthropic/claude-sonnet-4-6".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let config_json =
|
let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
|
||||||
build_spawn_config_json("child", "$yoi/default", &[], &model, false).unwrap();
|
|
||||||
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.model.ref_.as_deref(),
|
parsed.model.ref_.as_deref(),
|
||||||
@@ -1680,7 +1668,7 @@ max_tokens = 3333
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
let config_json =
|
let config_json =
|
||||||
build_spawn_config_json("child", "$yoi/default", &scope, &model, true).unwrap();
|
build_spawn_config_json("child", "default", &scope, &model, true).unwrap();
|
||||||
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.session.as_ref().and_then(|s| s.record_event_trace),
|
parsed.session.as_ref().and_then(|s| s.record_event_trace),
|
||||||
@@ -1700,8 +1688,7 @@ max_tokens = 3333
|
|||||||
ref_: Some("anthropic/claude-sonnet-4-6".into()),
|
ref_: Some("anthropic/claude-sonnet-4-6".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let config_json =
|
let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
|
||||||
build_spawn_config_json("child", "$yoi/default", &[], &model, false).unwrap();
|
|
||||||
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
|
||||||
|
|
||||||
assert!(parsed.session.is_none());
|
assert!(parsed.session.is_none());
|
||||||
@@ -1737,7 +1724,7 @@ max_tokens = 3333
|
|||||||
|
|
||||||
assert_eq!(config.worker.name.as_deref(), Some("child-default"));
|
assert_eq!(config.worker.name.as_deref(), Some("child-default"));
|
||||||
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
||||||
assert_eq!(config.engine.instruction.as_deref(), Some("$yoi/reviewer"));
|
assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
|
||||||
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
||||||
assert_eq!(config.scope.allow, scope);
|
assert_eq!(config.scope.allow, scope);
|
||||||
assert!(config.scope.deny.is_empty());
|
assert!(config.scope.deny.is_empty());
|
||||||
@@ -1776,7 +1763,7 @@ max_tokens = 3333
|
|||||||
|
|
||||||
assert_eq!(config.worker.name.as_deref(), Some("review-child"));
|
assert_eq!(config.worker.name.as_deref(), Some("review-child"));
|
||||||
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
||||||
assert_eq!(config.engine.instruction.as_deref(), Some("$yoi/reviewer"));
|
assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
|
||||||
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
||||||
assert_eq!(config.engine.max_tokens, Some(3333));
|
assert_eq!(config.engine.max_tokens, Some(3333));
|
||||||
assert_eq!(config.scope.allow, scope);
|
assert_eq!(config.scope.allow, scope);
|
||||||
@@ -1810,7 +1797,7 @@ max_tokens = 3333
|
|||||||
|
|
||||||
assert_eq!(config.worker.name.as_deref(), Some("inherited-child"));
|
assert_eq!(config.worker.name.as_deref(), Some("inherited-child"));
|
||||||
assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
|
assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
|
||||||
assert_eq!(config.engine.instruction.as_deref(), Some("$yoi/parent"));
|
assert_eq!(config.engine.instruction.as_deref(), Some("default"));
|
||||||
assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
|
assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
|
||||||
assert_eq!(config.engine.max_tokens, Some(1234));
|
assert_eq!(config.engine.max_tokens, Some(1234));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1845,15 +1832,12 @@ max_tokens = 3333
|
|||||||
&available,
|
&available,
|
||||||
&project,
|
&project,
|
||||||
"override-child",
|
"override-child",
|
||||||
Some("$user/custom-reviewer"),
|
Some("role.reviewer"),
|
||||||
&scope,
|
&scope,
|
||||||
SpawnProfileSelector::Default,
|
SpawnProfileSelector::Default,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
|
||||||
config.engine.instruction.as_deref(),
|
|
||||||
Some("$user/custom-reviewer")
|
|
||||||
);
|
|
||||||
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
|
||||||
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
|
||||||
assert_eq!(config.engine.max_tokens, Some(3333));
|
assert_eq!(config.engine.max_tokens, Some(3333));
|
||||||
|
|||||||
+22
-22
@@ -53,7 +53,7 @@ use crate::internal_worker::{
|
|||||||
const COMPACTION_EXTENSION_DOMAIN: &str = "yoi.compaction";
|
const COMPACTION_EXTENSION_DOMAIN: &str = "yoi.compaction";
|
||||||
const COMPACTION_BLOCK_ID: &str = "compact";
|
const COMPACTION_BLOCK_ID: &str = "compact";
|
||||||
const WORKER_ORCHESTRATION_INSTRUCTION_ID: &str = "worker.orchestration";
|
const WORKER_ORCHESTRATION_INSTRUCTION_ID: &str = "worker.orchestration";
|
||||||
const WORKER_ORCHESTRATION_PROMPT_REF: &str = "$yoi/common/worker-orchestration";
|
const WORKER_ORCHESTRATION_PROMPT_REF: &str = "common.worker_orchestration";
|
||||||
|
|
||||||
fn worker_orchestration_instruction() -> FeatureInstructionDeclaration {
|
fn worker_orchestration_instruction() -> FeatureInstructionDeclaration {
|
||||||
FeatureInstructionDeclaration::new(
|
FeatureInstructionDeclaration::new(
|
||||||
@@ -68,7 +68,7 @@ use crate::ipc::interceptor::WorkerInterceptor;
|
|||||||
use crate::ipc::notify_buffer::NotifyBuffer;
|
use crate::ipc::notify_buffer::NotifyBuffer;
|
||||||
use crate::prompt::agents_md::read_agents_md;
|
use crate::prompt::agents_md::read_agents_md;
|
||||||
use crate::prompt::catalog::{CatalogError, PromptCatalog};
|
use crate::prompt::catalog::{CatalogError, PromptCatalog};
|
||||||
use crate::prompt::loader::PromptLoader;
|
use crate::prompt::source::PromptCatalogSource;
|
||||||
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
||||||
use crate::runtime::dir;
|
use crate::runtime::dir;
|
||||||
use crate::runtime::worker_allocation::{self, ScopeAllocationGuard, ScopeLockError};
|
use crate::runtime::worker_allocation::{self, ScopeAllocationGuard, ScopeLockError};
|
||||||
@@ -4243,7 +4243,7 @@ where
|
|||||||
pub async fn from_manifest(
|
pub async fn from_manifest(
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
let cwd = current_cwd()?;
|
let cwd = current_cwd()?;
|
||||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||||
@@ -4255,7 +4255,7 @@ where
|
|||||||
pub async fn from_manifest_with_context(
|
pub async fn from_manifest_with_context(
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
@@ -4352,7 +4352,7 @@ where
|
|||||||
pub(crate) async fn from_internal_manifest_with_context(
|
pub(crate) async fn from_internal_manifest_with_context(
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
client_override: Option<Box<dyn LlmClient>>,
|
client_override: Option<Box<dyn LlmClient>>,
|
||||||
@@ -4435,7 +4435,7 @@ where
|
|||||||
pub async fn from_manifest_spawned(
|
pub async fn from_manifest_spawned(
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
let cwd = current_cwd()?;
|
let cwd = current_cwd()?;
|
||||||
@@ -4455,7 +4455,7 @@ where
|
|||||||
pub async fn from_manifest_spawned_with_context(
|
pub async fn from_manifest_spawned_with_context(
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
@@ -4544,7 +4544,7 @@ where
|
|||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
let cwd = current_cwd()?;
|
let cwd = current_cwd()?;
|
||||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||||
@@ -4564,7 +4564,7 @@ where
|
|||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
@@ -4613,7 +4613,7 @@ where
|
|||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
fallback: WorkerManifest,
|
fallback: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
@@ -4683,7 +4683,7 @@ where
|
|||||||
segment_id: SegmentId,
|
segment_id: SegmentId,
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
let cwd = current_cwd()?;
|
let cwd = current_cwd()?;
|
||||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||||
@@ -4705,7 +4705,7 @@ where
|
|||||||
segment_id: SegmentId,
|
segment_id: SegmentId,
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptCatalogSource,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
@@ -4903,7 +4903,7 @@ where
|
|||||||
pub async fn from_manifest_toml(toml: &str, store: St) -> Result<Self, WorkerError> {
|
pub async fn from_manifest_toml(toml: &str, store: St) -> Result<Self, WorkerError> {
|
||||||
let config = WorkerManifestConfig::from_toml(toml).map_err(WorkerError::ManifestParse)?;
|
let config = WorkerManifestConfig::from_toml(toml).map_err(WorkerError::ManifestParse)?;
|
||||||
let manifest = WorkerManifest::try_from(config).map_err(WorkerError::ManifestResolve)?;
|
let manifest = WorkerManifest::try_from(config).map_err(WorkerError::ManifestResolve)?;
|
||||||
Self::from_manifest(manifest, store, PromptLoader::builtins_only()).await
|
Self::from_manifest(manifest, store, PromptCatalogSource::builtins_only()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5588,7 +5588,7 @@ fn delegated_write_rule_to_deny(rule: WorkerSpawnedScopeRule) -> Option<ScopeRul
|
|||||||
/// a previously-rendered `system_prompt` verbatim.
|
/// a previously-rendered `system_prompt` verbatim.
|
||||||
fn prepare_worker_common_with_context(
|
fn prepare_worker_common_with_context(
|
||||||
manifest: &WorkerManifest,
|
manifest: &WorkerManifest,
|
||||||
loader: &PromptLoader,
|
loader: &PromptCatalogSource,
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
@@ -5633,7 +5633,7 @@ fn prepare_worker_common_with_context(
|
|||||||
|
|
||||||
fn prepare_worker_common_from_scope(
|
fn prepare_worker_common_from_scope(
|
||||||
manifest: &WorkerManifest,
|
manifest: &WorkerManifest,
|
||||||
loader: &PromptLoader,
|
loader: &PromptCatalogSource,
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
@@ -5655,7 +5655,7 @@ fn prepare_worker_common_from_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 = crate::model_client::build_client(&manifest.model)?;
|
||||||
let prompts = PromptCatalog::load(loader, manifest.worker.prompt_pack.as_deref())?;
|
let prompts = PromptCatalog::load(loader)?;
|
||||||
let system_prompt_template = if parse_template {
|
let system_prompt_template = if parse_template {
|
||||||
Some(
|
Some(
|
||||||
SystemPromptTemplate::parse(&manifest.engine.instruction, loader.clone())
|
SystemPromptTemplate::parse(&manifest.engine.instruction, loader.clone())
|
||||||
@@ -5706,7 +5706,7 @@ mod spawned_context_tests {
|
|||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
manifest.memory = Some(manifest::MemoryConfig::default());
|
||||||
let common = prepare_worker_common_with_context(
|
let common = prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&PromptLoader::builtins_only(),
|
&PromptCatalogSource::builtins_only(),
|
||||||
false,
|
false,
|
||||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||||
@@ -5739,7 +5739,7 @@ mod spawned_context_tests {
|
|||||||
std::fs::create_dir_all(&cwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
manifest.memory = Some(manifest::MemoryConfig::default());
|
||||||
let loader = PromptLoader::new(None, Some(workspace_root.clone()));
|
let loader = PromptCatalogSource::builtins_only();
|
||||||
let workspace_id = WorkspaceId::new("ws-api-only").unwrap();
|
let workspace_id = WorkspaceId::new("ws-api-only").unwrap();
|
||||||
let common = prepare_worker_common_with_context(
|
let common = prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
@@ -5776,7 +5776,7 @@ mod spawned_context_tests {
|
|||||||
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
let err = match prepare_worker_common_with_context(
|
let err = match prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&PromptLoader::builtins_only(),
|
&PromptCatalogSource::builtins_only(),
|
||||||
false,
|
false,
|
||||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||||
@@ -5812,7 +5812,7 @@ mod spawned_context_tests {
|
|||||||
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
let err = match prepare_worker_common_with_context(
|
let err = match prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&PromptLoader::builtins_only(),
|
&PromptCatalogSource::builtins_only(),
|
||||||
false,
|
false,
|
||||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap())),
|
||||||
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone()),
|
||||||
@@ -6888,8 +6888,8 @@ mod build_summary_prompt_tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
worker.set_resident_memory_injection(gates.summary);
|
worker.set_resident_memory_injection(gates.summary);
|
||||||
let template = SystemPromptTemplate::parse(
|
let template = SystemPromptTemplate::parse(
|
||||||
"$yoi/default",
|
"default",
|
||||||
crate::prompt::loader::PromptLoader::builtins_only(),
|
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
worker.set_system_prompt_template(template);
|
worker.set_system_prompt_template(template);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async fn restore_from_worker_metadata_rejects_missing_metadata() {
|
|||||||
"restore-test",
|
"restore-test",
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
worker::PromptLoader::builtins_only(),
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ async fn restore_from_worker_metadata_rejects_pending_segment() {
|
|||||||
"restore-test",
|
"restore-test",
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
worker::PromptLoader::builtins_only(),
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ async fn restore_from_worker_metadata_resolves_active_pointer_through_session_lo
|
|||||||
"restore-test",
|
"restore-test",
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
worker::PromptLoader::builtins_only(),
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ async fn restore_from_manifest_rejects_unknown_segment() {
|
|||||||
unknown_seg,
|
unknown_seg,
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
worker::PromptLoader::builtins_only(),
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ async fn restore_from_manifest_rejects_empty_segment_log() {
|
|||||||
segid,
|
segid,
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
worker::PromptLoader::builtins_only(),
|
worker::PromptCatalogSource::builtins_only(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ use llm_engine::llm_client::{ClientError, LlmClient, Request};
|
|||||||
use session_store::{CombinedStore, FsWorkerStore};
|
use session_store::{CombinedStore, FsWorkerStore};
|
||||||
use session_store::{FsStore, LogEntry, Store};
|
use session_store::{FsStore, LogEntry, Store};
|
||||||
|
|
||||||
use worker::{PromptLoader, SystemPromptTemplate, Worker, WorkerError};
|
use worker::{
|
||||||
|
EffectivePromptCatalog, PromptCatalog, PromptCatalogSource, SystemPromptTemplate, Worker,
|
||||||
|
WorkerError,
|
||||||
|
};
|
||||||
|
|
||||||
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
||||||
|
|
||||||
@@ -96,9 +99,9 @@ permission = "write"
|
|||||||
|
|
||||||
/// Build a Worker with a synthetic instruction template.
|
/// Build a Worker with a synthetic instruction template.
|
||||||
///
|
///
|
||||||
/// Writes `body` to a temp user-prompts dir under `$user/test`, builds a
|
/// Builds an immutable effective catalog with `body` at the exact `test`
|
||||||
/// PromptLoader pointing at it, parses the template, and installs it on
|
/// Prompt name and installs that parsed template on a directly constructed
|
||||||
/// a Worker constructed directly via `Worker::new`.
|
/// Worker.
|
||||||
async fn make_worker_with_body(
|
async fn make_worker_with_body(
|
||||||
body: &str,
|
body: &str,
|
||||||
client: MockClient,
|
client: MockClient,
|
||||||
@@ -117,10 +120,15 @@ async fn make_worker_with_body(
|
|||||||
let scope = worker::Scope::writable(&pwd).unwrap();
|
let scope = worker::Scope::writable(&pwd).unwrap();
|
||||||
std::mem::forget(pwd_tmp);
|
std::mem::forget(pwd_tmp);
|
||||||
|
|
||||||
let user_prompts_tmp = tempfile::tempdir().unwrap();
|
let mut templates = PromptCatalog::builtins_only()
|
||||||
std::fs::write(user_prompts_tmp.path().join("test.md"), body).unwrap();
|
.unwrap()
|
||||||
let loader = PromptLoader::new(Some(user_prompts_tmp.path().to_path_buf()), None);
|
.projection()
|
||||||
std::mem::forget(user_prompts_tmp);
|
.templates
|
||||||
|
.clone();
|
||||||
|
templates.insert("test".to_string(), body.to_string());
|
||||||
|
let projection =
|
||||||
|
EffectivePromptCatalog::new(templates, 1, "test-schema", "test-toolchain").unwrap();
|
||||||
|
let loader = PromptCatalogSource::builtins_only().with_effective_catalog(projection);
|
||||||
|
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let mut worker = Worker::new(
|
let mut worker = Worker::new(
|
||||||
@@ -133,7 +141,7 @@ async fn make_worker_with_body(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let template = SystemPromptTemplate::parse("$user/test", loader)
|
let template = SystemPromptTemplate::parse("test", loader)
|
||||||
.map_err(|source| WorkerError::InvalidSystemPromptTemplate { source })?;
|
.map_err(|source| WorkerError::InvalidSystemPromptTemplate { source })?;
|
||||||
worker.set_system_prompt_template(template);
|
worker.set_system_prompt_template(template);
|
||||||
|
|
||||||
@@ -146,15 +154,15 @@ async fn make_worker_with_body(
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn template_parse_rejects_invalid_syntax() {
|
async fn template_parse_rejects_invalid_syntax() {
|
||||||
let user_prompts_tmp = tempfile::tempdir().unwrap();
|
let mut templates = PromptCatalog::builtins_only()
|
||||||
std::fs::write(user_prompts_tmp.path().join("broken.md"), "{{ unclosed").unwrap();
|
.unwrap()
|
||||||
let loader = PromptLoader::new(Some(user_prompts_tmp.path().to_path_buf()), None);
|
.projection()
|
||||||
let err = SystemPromptTemplate::parse("$user/broken", loader).unwrap_err();
|
.templates
|
||||||
let worker_err: WorkerError = WorkerError::InvalidSystemPromptTemplate { source: err };
|
.clone();
|
||||||
assert!(matches!(
|
templates.insert("broken".to_string(), "{{ unclosed".to_string());
|
||||||
worker_err,
|
let error =
|
||||||
WorkerError::InvalidSystemPromptTemplate { .. }
|
EffectivePromptCatalog::new(templates, 1, "test-schema", "test-toolchain").unwrap_err();
|
||||||
));
|
assert!(error.to_string().contains("does not compile"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -121,19 +121,19 @@ root = ".yoi/tickets"
|
|||||||
|
|
||||||
[ticket.roles.intake]
|
[ticket.roles.intake]
|
||||||
profile = "project:intake"
|
profile = "project:intake"
|
||||||
launch_prompt = "$workspace/ticket/intake/launch"
|
launch_prompt = "ticket.intake.launch"
|
||||||
|
|
||||||
[ticket.roles.orchestrator]
|
[ticket.roles.orchestrator]
|
||||||
profile = "project:orchestrator"
|
profile = "project:orchestrator"
|
||||||
launch_prompt = "$workspace/ticket/orchestrator/launch"
|
launch_prompt = "ticket.orchestrator.launch"
|
||||||
|
|
||||||
[ticket.roles.coder]
|
[ticket.roles.coder]
|
||||||
profile = "project:coder"
|
profile = "project:coder"
|
||||||
launch_prompt = "$workspace/ticket/coder/launch"
|
launch_prompt = "ticket.coder.launch"
|
||||||
|
|
||||||
[ticket.roles.reviewer]
|
[ticket.roles.reviewer]
|
||||||
profile = "project:reviewer"
|
profile = "project:reviewer"
|
||||||
launch_prompt = "$workspace/ticket/reviewer/launch"
|
launch_prompt = "ticket.reviewer.launch"
|
||||||
```
|
```
|
||||||
|
|
||||||
Fixed roles are:
|
Fixed roles are:
|
||||||
|
|||||||
+3
-10
@@ -30,12 +30,6 @@
|
|||||||
# 必須。Worker の表示名 (ResolveError::MissingField("worker.name") の対象)。
|
# 必須。Worker の表示名 (ResolveError::MissingField("worker.name") の対象)。
|
||||||
name = "example-agent"
|
name = "example-agent"
|
||||||
|
|
||||||
# 任意。デフォルト: なし。
|
|
||||||
# PromptCatalog の 4 つ目の overlay 層として読み込む TOML pack のパス。
|
|
||||||
# 相対パスは manifest base 起点で解決。`worker.instruction` (`$prefix/...`)
|
|
||||||
# とは別系統の単なるファイルパス。
|
|
||||||
# prompt_pack = "./prompts.local.toml"
|
|
||||||
|
|
||||||
|
|
||||||
# ===== [model] ==============================================================
|
# ===== [model] ==============================================================
|
||||||
# LLM モデル設定。次の 3 形態を受ける:
|
# LLM モデル設定。次の 3 形態を受ける:
|
||||||
@@ -95,10 +89,9 @@ ref = "anthropic/claude-sonnet-4-6"
|
|||||||
# ワーカーの生成パラメータ等。セクション自体省略可 (全フィールド任意)。
|
# ワーカーの生成パラメータ等。セクション自体省略可 (全フィールド任意)。
|
||||||
[engine]
|
[engine]
|
||||||
|
|
||||||
# 任意。デフォルト: "$yoi/default" (`defaults::DEFAULT_INSTRUCTION`)。
|
# 任意。デフォルト: "default" (`defaults::DEFAULT_INSTRUCTION`)。
|
||||||
# システムプロンプト本体の `PromptLoader` 参照。
|
# effective Prompt catalog の exact dotted name を選択する。
|
||||||
# プレフィクス: "$yoi/..." | "$user/..." | "$workspace/..."
|
# instruction = "default"
|
||||||
# instruction = "$yoi/default"
|
|
||||||
|
|
||||||
# 任意。デフォルト: なし (プロバイダ任せ)。
|
# 任意。デフォルト: なし (プロバイダ任せ)。
|
||||||
# 1 レスポンスあたりの出力 token 上限。
|
# 1 レスポンスあたりの出力 token 上限。
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./base.dcdl" // {
|
|||||||
slug = "coder";
|
slug = "coder";
|
||||||
description = "Ticket implementation coder profile.";
|
description = "Ticket implementation coder profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
engine = { instruction = "role.coder"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./base.dcdl" // {
|
|||||||
slug = "intake";
|
slug = "intake";
|
||||||
description = "Ticket intake profile.";
|
description = "Ticket intake profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
engine = { instruction = "role.intake"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./base.dcdl" // {
|
|||||||
slug = "orchestrator";
|
slug = "orchestrator";
|
||||||
description = "Ticket orchestrator profile.";
|
description = "Ticket orchestrator profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
engine = { instruction = "role.orchestrator"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./base.dcdl" // {
|
|||||||
slug = "reviewer";
|
slug = "reviewer";
|
||||||
description = "Ticket review profile.";
|
description = "Ticket review profile.";
|
||||||
scope = "workspace_read";
|
scope = "workspace_read";
|
||||||
|
engine = { instruction = "role.reviewer"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Deterministic builtin Prompt source tree. Markdown imports use the shared
|
||||||
|
# { frontmatter, content } contract; every leaf below selects only .content.
|
||||||
|
# `default_prompt` is the DCDL source alias for the effective catalog's
|
||||||
|
# reserved dotted name `default`.
|
||||||
|
let
|
||||||
|
defaultDocument = import "./default.md";
|
||||||
|
commonLanguage = import "./common/language.md";
|
||||||
|
commonTickets = import "./common/tickets.md";
|
||||||
|
commonToolUsage = import "./common/tool-usage.md";
|
||||||
|
commonWorkerObservation = import "./common/worker-observation.md";
|
||||||
|
commonWorkerOrchestration = import "./common/worker-orchestration.md";
|
||||||
|
commonWorkspace = import "./common/workspace.md";
|
||||||
|
commonWriting = import "./common/writing.md";
|
||||||
|
roleCoder = import "./role/coder.md";
|
||||||
|
roleIntake = import "./role/intake.md";
|
||||||
|
roleOrchestrator = import "./role/orchestrator.md";
|
||||||
|
roleReviewer = import "./role/reviewer.md";
|
||||||
|
internalCompactSystem = import "./internal/compact_system.md";
|
||||||
|
internalFlowVerifierSystem = import "./internal/flow_verifier_system.md";
|
||||||
|
internalMemoryConsolidationSystem = import "./internal/memory_consolidation_system.md";
|
||||||
|
internalMemoryExtractSystem = import "./internal/memory_extract_system.md";
|
||||||
|
internalWorkspaceOrchestratorQueueAttention = import "./internal/workspace_orchestrator_queue_attention.md";
|
||||||
|
internalNotifyWrapper = import "./internal/notify_wrapper.md";
|
||||||
|
internalInterruptToolResultSummary = import "./internal/interrupt_tool_result_summary.md";
|
||||||
|
internalInterruptSystemNote = import "./internal/interrupt_system_note.md";
|
||||||
|
internalWorkingBoundariesSection = import "./internal/working_boundaries_section.md";
|
||||||
|
internalAgentsMdSection = import "./internal/agents_md_section.md";
|
||||||
|
internalResidentMemorySummarySection = import "./internal/resident_memory_summary_section.md";
|
||||||
|
internalSubWorkerSpawnToolDescription = import "./internal/sub_worker_spawn_tool_description.md";
|
||||||
|
panelOrchestratorIdleQueueNotice = import "./panel/orchestrator_idle_queue_notice.md";
|
||||||
|
workerTicketEventCompanionNotice = import "./worker/ticket_event_companion_notice.md";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default_prompt = defaultDocument.content;
|
||||||
|
common = {
|
||||||
|
language = commonLanguage.content;
|
||||||
|
tickets = commonTickets.content;
|
||||||
|
tool_usage = commonToolUsage.content;
|
||||||
|
worker_observation = commonWorkerObservation.content;
|
||||||
|
worker_orchestration = commonWorkerOrchestration.content;
|
||||||
|
workspace = commonWorkspace.content;
|
||||||
|
writing = commonWriting.content;
|
||||||
|
};
|
||||||
|
role = {
|
||||||
|
coder = roleCoder.content;
|
||||||
|
intake = roleIntake.content;
|
||||||
|
orchestrator = roleOrchestrator.content;
|
||||||
|
reviewer = roleReviewer.content;
|
||||||
|
};
|
||||||
|
internal = {
|
||||||
|
compact_system = internalCompactSystem.content;
|
||||||
|
flow_verifier_system = internalFlowVerifierSystem.content;
|
||||||
|
memory_consolidation_system = internalMemoryConsolidationSystem.content;
|
||||||
|
memory_extract_system = internalMemoryExtractSystem.content;
|
||||||
|
workspace_orchestrator_queue_attention = internalWorkspaceOrchestratorQueueAttention.content;
|
||||||
|
notify_wrapper = internalNotifyWrapper.content;
|
||||||
|
interrupt_tool_result_summary = internalInterruptToolResultSummary.content;
|
||||||
|
interrupt_system_note = internalInterruptSystemNote.content;
|
||||||
|
working_boundaries_section = internalWorkingBoundariesSection.content;
|
||||||
|
agents_md_section = internalAgentsMdSection.content;
|
||||||
|
resident_memory_summary_section = internalResidentMemorySummarySection.content;
|
||||||
|
worker_orchestration_guidance_section = commonWorkerOrchestration.content;
|
||||||
|
sub_worker_spawn_tool_description = internalSubWorkerSpawnToolDescription.content;
|
||||||
|
};
|
||||||
|
panel = {
|
||||||
|
orchestrator_idle_queue_notice = panelOrchestratorIdleQueueNotice.content;
|
||||||
|
};
|
||||||
|
worker = {
|
||||||
|
ticket_event_companion_notice = workerTicketEventCompanionNotice.content;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
## SubWorker orchestration
|
## SubWorker orchestration
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ You are here as an agent of the "yoi system".
|
|||||||
|
|
||||||
Stay precise, edit code directly when asked, and avoid speculative refactoring.
|
Stay precise, edit code directly when asked, and avoid speculative refactoring.
|
||||||
|
|
||||||
{% include "common/workspace" %}
|
{% include "common.workspace" %}
|
||||||
|
|
||||||
{% include "common/tool-usage" %}
|
{% include "common.tool_usage" %}
|
||||||
|
|
||||||
|
|
||||||
{% include "common/language" %}
|
{% include "common.language" %}
|
||||||
|
|
||||||
{% include "common/writing" %}
|
{% include "common.writing" %}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Worker internal prompts (builtin pack).
|
|
||||||
#
|
|
||||||
# Values are minijinja template strings. Use `{% include "$prefix/..." %}`
|
|
||||||
# to pull in long text from the $yoi / $user / $workspace prompt
|
|
||||||
# libraries.
|
|
||||||
#
|
|
||||||
# Every key here MUST correspond to a `WorkerPrompt` variant; missing or
|
|
||||||
# extra keys cause a build-time error (see `crates/worker/build.rs`).
|
|
||||||
|
|
||||||
[prompt]
|
|
||||||
compact_system = "{% include \"$yoi/internal/compact_system\" %}"
|
|
||||||
|
|
||||||
memory_extract_system = "{% include \"$yoi/internal/memory_extract_system\" %}"
|
|
||||||
|
|
||||||
memory_consolidation_system = "{% include \"$yoi/internal/memory_consolidation_system\" %}"
|
|
||||||
|
|
||||||
flow_verifier_system = "{% include \"$yoi/internal/flow_verifier_system\" %}"
|
|
||||||
|
|
||||||
notify_wrapper = """\
|
|
||||||
[Notification]
|
|
||||||
{{ message }}
|
|
||||||
|
|
||||||
This is a notification, not a blocking request. If you are in the middle of a task, continue your current work and address this at a natural stopping point.\
|
|
||||||
"""
|
|
||||||
|
|
||||||
interrupt_tool_result_summary = "[Interrupted by user]"
|
|
||||||
|
|
||||||
interrupt_system_note = "[The previous turn was interrupted by the user. The user's next request follows.]"
|
|
||||||
|
|
||||||
working_boundaries_section = """\
|
|
||||||
---
|
|
||||||
## Working boundaries
|
|
||||||
|
|
||||||
{{ scope_summary }}\
|
|
||||||
"""
|
|
||||||
|
|
||||||
agents_md_section = """\
|
|
||||||
---
|
|
||||||
## Project instructions (AGENTS.md)
|
|
||||||
|
|
||||||
{{ agents_md }}\
|
|
||||||
"""
|
|
||||||
|
|
||||||
resident_memory_summary_section = """\
|
|
||||||
---
|
|
||||||
## Resident memory summary
|
|
||||||
|
|
||||||
The following is the current durable session/workspace summary. Treat it as background context; it is not a user request.
|
|
||||||
|
|
||||||
{{ summary }}\
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
worker_orchestration_guidance_section = "{% include \"$yoi/common/worker-orchestration\" %}"
|
|
||||||
|
|
||||||
ticket_event_companion_notice = "{% include \"$yoi/worker/ticket_event_companion_notice\" %}"
|
|
||||||
|
|
||||||
sub_worker_spawn_tool_description = """\
|
|
||||||
Spawn a parent-owned Internal SubWorker session to split context for a delegated task. The parent Worker's write scope is reduced by the scope passed here; the Internal SubWorker starts running `task` immediately without creating a Runtime Worker record, OS process, PID, or Unix socket. It remains available for follow-up turns until explicitly stopped or its parent exits.
|
|
||||||
|
|
||||||
Optional `cwd`: when provided, it is the Internal SubWorker's tool default working directory only. It must be an absolute existing directory covered by the child's delegated readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children.
|
|
||||||
|
|
||||||
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Worker, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is always the only delegated filesystem capability; profile scope is replaced by the explicit SubWorkerSpawn scope.
|
|
||||||
|
|
||||||
Default profile: {{ default_profile }}
|
|
||||||
Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope.
|
|
||||||
Available registry profiles:
|
|
||||||
{{ available_profiles }}{% if profile_diagnostic %}
|
|
||||||
|
|
||||||
Profile discovery diagnostic: {{ profile_diagnostic }}{% endif %}\
|
|
||||||
"""
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
## Project instructions (AGENTS.md)
|
||||||
|
|
||||||
|
{{ agents_md }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[The previous turn was interrupted by the user. The user's next request follows.]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[Interrupted by user]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[Notification]
|
||||||
|
{{ message }}
|
||||||
|
|
||||||
|
This is a notification, not a blocking request. If you are in the middle of a task, continue your current work and address this at a natural stopping point.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
## Resident memory summary
|
||||||
|
|
||||||
|
The following is the current durable session/workspace summary. Treat it as background context; it is not a user request.
|
||||||
|
|
||||||
|
{{ summary }}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Spawn a parent-owned Internal SubWorker session to split context for a delegated task. The parent Worker's write scope is reduced by the scope passed here; the Internal SubWorker starts running `task` immediately without creating a Runtime Worker record, OS process, PID, or Unix socket. It remains available for follow-up turns until explicitly stopped or its parent exits.
|
||||||
|
|
||||||
|
Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be an absolute existing directory covered by the child's delegated readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children.
|
||||||
|
|
||||||
|
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Worker, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is always the only delegated filesystem capability; profile scope is replaced by the explicit SubWorkerSpawn scope.
|
||||||
|
|
||||||
|
Default profile: {{ default_profile }}
|
||||||
|
Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope.
|
||||||
|
Available registry profiles:
|
||||||
|
{{ available_profiles }}{% if profile_diagnostic %}
|
||||||
|
|
||||||
|
Profile discovery diagnostic: {{ profile_diagnostic }}{% endif %}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
## Working boundaries
|
||||||
|
|
||||||
|
{{ scope_summary }}
|
||||||
Reference in New Issue
Block a user