prompt: derive guidance from features
This commit is contained in:
parent
9125cee6fd
commit
44e7014d83
|
|
@ -663,9 +663,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if feature_config.workers.enabled {
|
||||||
|
worker.register_worker_orchestration_instruction();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let workspace_client = worker.workspace_client().clone();
|
let workspace_client = worker.workspace_client().clone();
|
||||||
let worker = worker.engine_mut();
|
let engine = worker.engine_mut();
|
||||||
|
|
||||||
// Objective tools expose read-only project Objective context through the
|
// Objective tools expose read-only project Objective context through the
|
||||||
// Backend Workspace API. Workers must not guess local `.yoi/objectives`
|
// Backend Workspace API. Workers must not guess local `.yoi/objectives`
|
||||||
|
|
@ -680,7 +684,7 @@ where
|
||||||
workspace_id.clone(),
|
workspace_id.clone(),
|
||||||
base_url.clone(),
|
base_url.clone(),
|
||||||
) {
|
) {
|
||||||
worker.register_tool(definition);
|
engine.register_tool(definition);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
|
|
@ -718,7 +722,7 @@ where
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
for definition in definitions {
|
for definition in definitions {
|
||||||
worker.register_tool(definition);
|
engine.register_tool(definition);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
|
|
@ -755,7 +759,7 @@ where
|
||||||
"worker spawn tools require local Worker filesystem authority",
|
"worker spawn tools require local Worker filesystem authority",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
worker.register_tool(spawn_worker_tool(
|
engine.register_tool(spawn_worker_tool(
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
|
|
@ -767,9 +771,9 @@ where
|
||||||
scope_handle,
|
scope_handle,
|
||||||
prompts,
|
prompts,
|
||||||
));
|
));
|
||||||
worker.register_tool(send_to_worker_tool(spawned_registry.clone()));
|
engine.register_tool(send_to_worker_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(read_worker_output_tool(spawned_registry.clone()));
|
engine.register_tool(read_worker_output_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(stop_worker_tool(spawned_registry.clone()));
|
engine.register_tool(stop_worker_tool(spawned_registry.clone()));
|
||||||
let discovery = WorkerDiscovery::new(
|
let discovery = WorkerDiscovery::new(
|
||||||
worker_metadata_store,
|
worker_metadata_store,
|
||||||
spawner_name,
|
spawner_name,
|
||||||
|
|
@ -777,9 +781,9 @@ where
|
||||||
Some(spawner_cwd),
|
Some(spawner_cwd),
|
||||||
spawned_registry,
|
spawned_registry,
|
||||||
);
|
);
|
||||||
worker.register_tool(list_workers_tool(discovery.clone()));
|
engine.register_tool(list_workers_tool(discovery.clone()));
|
||||||
worker.register_tool(restore_worker_tool(discovery.clone()));
|
engine.register_tool(restore_worker_tool(discovery.clone()));
|
||||||
worker.register_tool(send_to_peer_worker_tool(discovery));
|
engine.register_tool(send_to_peer_worker_tool(discovery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _feature_install_report = worker.install_features(feature_registry);
|
let _feature_install_report = worker.install_features(feature_registry);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
//! [`crate::hook::HookRegistryBuilder`], and provider output is represented as
|
//! [`crate::hook::HookRegistryBuilder`], and provider output is represented as
|
||||||
//! ordinary feature reports/diagnostics instead of a separate authority layer.
|
//! ordinary feature reports/diagnostics instead of a separate authority layer.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -311,6 +311,83 @@ impl HookDeclaration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub struct FeatureInstructionId(String);
|
||||||
|
|
||||||
|
impl FeatureInstructionId {
|
||||||
|
pub fn new(value: impl Into<String>) -> Result<Self, FeatureInstallError> {
|
||||||
|
let value = value.into();
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Err(FeatureInstallError::InvalidDescriptor(
|
||||||
|
"feature instruction id must not be empty".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builtin(slug: impl AsRef<str>) -> Self {
|
||||||
|
Self(format!("builtin:{}", slug.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FeatureInstructionId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FeatureInstructionOrder {
|
||||||
|
OrchestrationPolicy,
|
||||||
|
WorkflowPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct FeatureInstructionDeclaration {
|
||||||
|
pub id: FeatureInstructionId,
|
||||||
|
pub prompt_ref: String,
|
||||||
|
pub order: FeatureInstructionOrder,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureInstructionDeclaration {
|
||||||
|
pub fn new(
|
||||||
|
id: FeatureInstructionId,
|
||||||
|
prompt_ref: impl Into<String>,
|
||||||
|
order: FeatureInstructionOrder,
|
||||||
|
description: impl Into<String>,
|
||||||
|
) -> Result<Self, FeatureInstallError> {
|
||||||
|
let prompt_ref = prompt_ref.into();
|
||||||
|
if prompt_ref.trim().is_empty() {
|
||||||
|
return Err(FeatureInstallError::InvalidDescriptor(
|
||||||
|
"feature instruction prompt_ref must not be empty".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
prompt_ref,
|
||||||
|
order,
|
||||||
|
description: description.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct FeatureInstructionContribution {
|
||||||
|
pub declaration: FeatureInstructionDeclaration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureInstructionContribution {
|
||||||
|
pub fn new(declaration: FeatureInstructionDeclaration) -> Self {
|
||||||
|
Self { declaration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Background task lifecycle phase represented by this registry slice.
|
/// Background task lifecycle phase represented by this registry slice.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
@ -485,6 +562,7 @@ pub struct FeatureDescriptor {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub tools: Vec<ToolDeclaration>,
|
pub tools: Vec<ToolDeclaration>,
|
||||||
pub hooks: Vec<HookDeclaration>,
|
pub hooks: Vec<HookDeclaration>,
|
||||||
|
pub instructions: Vec<FeatureInstructionDeclaration>,
|
||||||
pub background_tasks: Vec<BackgroundTaskDeclaration>,
|
pub background_tasks: Vec<BackgroundTaskDeclaration>,
|
||||||
pub provides_services: Vec<ServiceDeclaration>,
|
pub provides_services: Vec<ServiceDeclaration>,
|
||||||
pub requires_services: Vec<ServiceRequirement>,
|
pub requires_services: Vec<ServiceRequirement>,
|
||||||
|
|
@ -501,6 +579,7 @@ impl FeatureDescriptor {
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
tools: Vec::new(),
|
tools: Vec::new(),
|
||||||
hooks: Vec::new(),
|
hooks: Vec::new(),
|
||||||
|
instructions: Vec::new(),
|
||||||
background_tasks: Vec::new(),
|
background_tasks: Vec::new(),
|
||||||
provides_services: Vec::new(),
|
provides_services: Vec::new(),
|
||||||
requires_services: Vec::new(),
|
requires_services: Vec::new(),
|
||||||
|
|
@ -523,6 +602,11 @@ impl FeatureDescriptor {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_instruction(mut self, instruction: FeatureInstructionDeclaration) -> Self {
|
||||||
|
self.instructions.push(instruction);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_background_task(mut self, task: BackgroundTaskDeclaration) -> Self {
|
pub fn with_background_task(mut self, task: BackgroundTaskDeclaration) -> Self {
|
||||||
self.background_tasks.push(task);
|
self.background_tasks.push(task);
|
||||||
self
|
self
|
||||||
|
|
@ -595,6 +679,7 @@ impl FeatureDiagnostic {
|
||||||
pub enum FeatureContributionKind {
|
pub enum FeatureContributionKind {
|
||||||
Tool,
|
Tool,
|
||||||
Hook,
|
Hook,
|
||||||
|
Instruction,
|
||||||
BackgroundTask,
|
BackgroundTask,
|
||||||
Service,
|
Service,
|
||||||
ProtocolProvider,
|
ProtocolProvider,
|
||||||
|
|
@ -619,6 +704,7 @@ pub struct FeatureInstallReport {
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
pub installed_tools: Vec<String>,
|
pub installed_tools: Vec<String>,
|
||||||
pub installed_hooks: Vec<HookDeclaration>,
|
pub installed_hooks: Vec<HookDeclaration>,
|
||||||
|
pub installed_instructions: Vec<FeatureInstructionDeclaration>,
|
||||||
pub declared_background_tasks: Vec<BackgroundTaskDeclaration>,
|
pub declared_background_tasks: Vec<BackgroundTaskDeclaration>,
|
||||||
pub provided_services: Vec<ServiceDeclaration>,
|
pub provided_services: Vec<ServiceDeclaration>,
|
||||||
pub resolved_service_requirements: Vec<ServiceRequirement>,
|
pub resolved_service_requirements: Vec<ServiceRequirement>,
|
||||||
|
|
@ -635,6 +721,7 @@ impl FeatureInstallReport {
|
||||||
installed: false,
|
installed: false,
|
||||||
installed_tools: Vec::new(),
|
installed_tools: Vec::new(),
|
||||||
installed_hooks: Vec::new(),
|
installed_hooks: Vec::new(),
|
||||||
|
installed_instructions: Vec::new(),
|
||||||
declared_background_tasks: Vec::new(),
|
declared_background_tasks: Vec::new(),
|
||||||
provided_services: Vec::new(),
|
provided_services: Vec::new(),
|
||||||
resolved_service_requirements: Vec::new(),
|
resolved_service_requirements: Vec::new(),
|
||||||
|
|
@ -662,6 +749,7 @@ impl FeatureInstallReport {
|
||||||
struct FeatureContributionDeclarations {
|
struct FeatureContributionDeclarations {
|
||||||
tools: HashSet<String>,
|
tools: HashSet<String>,
|
||||||
hooks: HashSet<(String, FeatureHookPoint)>,
|
hooks: HashSet<(String, FeatureHookPoint)>,
|
||||||
|
instructions: HashSet<FeatureInstructionId>,
|
||||||
background_tasks: HashSet<String>,
|
background_tasks: HashSet<String>,
|
||||||
provided_services: HashSet<(ServiceId, String)>,
|
provided_services: HashSet<(ServiceId, String)>,
|
||||||
protocol_providers: HashSet<ProviderId>,
|
protocol_providers: HashSet<ProviderId>,
|
||||||
|
|
@ -680,6 +768,11 @@ impl FeatureContributionDeclarations {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|hook| (hook.name.clone(), hook.point.clone()))
|
.map(|hook| (hook.name.clone(), hook.point.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
instructions: descriptor
|
||||||
|
.instructions
|
||||||
|
.iter()
|
||||||
|
.map(|instruction| instruction.id.clone())
|
||||||
|
.collect(),
|
||||||
background_tasks: descriptor
|
background_tasks: descriptor
|
||||||
.background_tasks
|
.background_tasks
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -707,6 +800,10 @@ impl FeatureContributionDeclarations {
|
||||||
.contains(&(declaration.name.clone(), declaration.point.clone()))
|
.contains(&(declaration.name.clone(), declaration.point.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn contains_instruction(&self, declaration: &FeatureInstructionDeclaration) -> bool {
|
||||||
|
self.instructions.contains(&declaration.id)
|
||||||
|
}
|
||||||
|
|
||||||
fn contains_background_task(&self, declaration: &BackgroundTaskDeclaration) -> bool {
|
fn contains_background_task(&self, declaration: &BackgroundTaskDeclaration) -> bool {
|
||||||
self.background_tasks.contains(&declaration.name)
|
self.background_tasks.contains(&declaration.name)
|
||||||
}
|
}
|
||||||
|
|
@ -947,6 +1044,39 @@ impl HookContributionRegistrar<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt instruction registrar for mandatory feature guidance contributions.
|
||||||
|
pub struct FeatureInstructionRegistrar<'a> {
|
||||||
|
feature_id: &'a FeatureId,
|
||||||
|
declarations: &'a FeatureContributionDeclarations,
|
||||||
|
report: &'a mut FeatureInstallReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureInstructionRegistrar<'_> {
|
||||||
|
pub fn register(
|
||||||
|
&mut self,
|
||||||
|
contribution: FeatureInstructionContribution,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
let declaration = contribution.declaration;
|
||||||
|
if !self.declarations.contains_instruction(&declaration) {
|
||||||
|
return Err(reject_undeclared_contribution(
|
||||||
|
self.feature_id,
|
||||||
|
self.report,
|
||||||
|
FeatureContributionKind::Instruction,
|
||||||
|
declaration.id.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !self
|
||||||
|
.report
|
||||||
|
.installed_instructions
|
||||||
|
.iter()
|
||||||
|
.any(|instruction| instruction.id == declaration.id)
|
||||||
|
{
|
||||||
|
self.report.installed_instructions.push(declaration);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Background task registrar for descriptor/report-only contributions.
|
/// Background task registrar for descriptor/report-only contributions.
|
||||||
pub struct BackgroundTaskRegistrar<'a> {
|
pub struct BackgroundTaskRegistrar<'a> {
|
||||||
feature_id: &'a FeatureId,
|
feature_id: &'a FeatureId,
|
||||||
|
|
@ -1184,6 +1314,14 @@ impl FeatureInstallContext<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn instructions(&mut self) -> FeatureInstructionRegistrar<'_> {
|
||||||
|
FeatureInstructionRegistrar {
|
||||||
|
feature_id: self.feature_id,
|
||||||
|
declarations: self.declarations,
|
||||||
|
report: self.report,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
|
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
|
||||||
BackgroundTaskRegistrar {
|
BackgroundTaskRegistrar {
|
||||||
feature_id: self.feature_id,
|
feature_id: self.feature_id,
|
||||||
|
|
@ -1245,6 +1383,26 @@ impl FeatureRegistryInstallReport {
|
||||||
.flat_map(|report| report.installed_tools.iter().cloned())
|
.flat_map(|report| report.installed_tools.iter().cloned())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn installed_instruction_contributions(&self) -> Vec<FeatureInstructionDeclaration> {
|
||||||
|
dedupe_instruction_contributions(
|
||||||
|
self.reports
|
||||||
|
.iter()
|
||||||
|
.flat_map(|report| report.installed_instructions.iter().cloned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dedupe_instruction_contributions(
|
||||||
|
instructions: impl IntoIterator<Item = FeatureInstructionDeclaration>,
|
||||||
|
) -> Vec<FeatureInstructionDeclaration> {
|
||||||
|
let mut by_id = BTreeMap::new();
|
||||||
|
for instruction in instructions {
|
||||||
|
by_id.entry(instruction.id.clone()).or_insert(instruction);
|
||||||
|
}
|
||||||
|
let mut instructions = by_id.into_values().collect::<Vec<_>>();
|
||||||
|
instructions.sort_by(|a, b| a.order.cmp(&b.order).then_with(|| a.id.cmp(&b.id)));
|
||||||
|
instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder/installer for enabled feature modules.
|
/// Builder/installer for enabled feature modules.
|
||||||
|
|
@ -1550,6 +1708,42 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InstructionFeature {
|
||||||
|
descriptor: FeatureDescriptor,
|
||||||
|
instruction: FeatureInstructionDeclaration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureModule for InstructionFeature {
|
||||||
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
|
self.descriptor.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install(
|
||||||
|
&self,
|
||||||
|
context: &mut FeatureInstallContext<'_>,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
context
|
||||||
|
.instructions()
|
||||||
|
.register(FeatureInstructionContribution::new(
|
||||||
|
self.instruction.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instruction(
|
||||||
|
id: &'static str,
|
||||||
|
prompt_ref: &'static str,
|
||||||
|
order: FeatureInstructionOrder,
|
||||||
|
) -> FeatureInstructionDeclaration {
|
||||||
|
FeatureInstructionDeclaration::new(
|
||||||
|
FeatureInstructionId::builtin(id),
|
||||||
|
prompt_ref,
|
||||||
|
order,
|
||||||
|
"test instruction",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn descriptor_contributions_are_recorded() {
|
fn descriptor_contributions_are_recorded() {
|
||||||
let descriptor = FeatureDescriptor::builtin("dummy", "Dummy")
|
let descriptor = FeatureDescriptor::builtin("dummy", "Dummy")
|
||||||
|
|
@ -1576,6 +1770,58 @@ mod tests {
|
||||||
assert_eq!(feature_report.declared_background_tasks[0].name, "daily");
|
assert_eq!(feature_report.declared_background_tasks[0].name, "daily");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instruction_contributions_are_deduped_and_sorted() {
|
||||||
|
let workflow = instruction(
|
||||||
|
"workflow",
|
||||||
|
"$yoi/common/tickets",
|
||||||
|
FeatureInstructionOrder::WorkflowPolicy,
|
||||||
|
);
|
||||||
|
let orchestration = instruction(
|
||||||
|
"orchestration",
|
||||||
|
"$yoi/common/worker-orchestration",
|
||||||
|
FeatureInstructionOrder::OrchestrationPolicy,
|
||||||
|
);
|
||||||
|
let contributions = dedupe_instruction_contributions([
|
||||||
|
workflow.clone(),
|
||||||
|
orchestration.clone(),
|
||||||
|
workflow.clone(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(contributions, vec![orchestration, workflow]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undeclared_instruction_contribution_is_rejected() {
|
||||||
|
let declared = instruction(
|
||||||
|
"declared",
|
||||||
|
"$yoi/common/tickets",
|
||||||
|
FeatureInstructionOrder::WorkflowPolicy,
|
||||||
|
);
|
||||||
|
let undeclared = instruction(
|
||||||
|
"undeclared",
|
||||||
|
"$yoi/common/tickets",
|
||||||
|
FeatureInstructionOrder::WorkflowPolicy,
|
||||||
|
);
|
||||||
|
let descriptor =
|
||||||
|
FeatureDescriptor::builtin("instruction", "Instruction").with_instruction(declared);
|
||||||
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(InstructionFeature {
|
||||||
|
descriptor,
|
||||||
|
instruction: undeclared,
|
||||||
|
})
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
|
||||||
|
assert!(!report.reports[0].installed);
|
||||||
|
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic
|
||||||
|
.message
|
||||||
|
.contains("undeclared Instruction contribution")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_tool_names_are_rejected() {
|
fn duplicate_tool_names_are_rejected() {
|
||||||
let descriptor_a = FeatureDescriptor::builtin("a", "A")
|
let descriptor_a = FeatureDescriptor::builtin("a", "A")
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,26 @@ use ticket::{
|
||||||
|
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError,
|
FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError,
|
||||||
FeatureModule, ToolContribution, ToolDeclaration,
|
FeatureInstructionContribution, FeatureInstructionDeclaration, FeatureInstructionId,
|
||||||
|
FeatureInstructionOrder, FeatureModule, ToolContribution, ToolDeclaration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEATURE_ID: &str = "ticket";
|
const FEATURE_ID: &str = "ticket";
|
||||||
const FEATURE_NAME: &str = "Ticket tools";
|
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_PROMPT_REF: &str = "$yoi/common/tickets";
|
||||||
|
|
||||||
|
fn ticket_workflow_instruction() -> FeatureInstructionDeclaration {
|
||||||
|
FeatureInstructionDeclaration::new(
|
||||||
|
FeatureInstructionId::builtin(TICKET_WORKFLOW_INSTRUCTION_ID),
|
||||||
|
TICKET_WORKFLOW_PROMPT_REF,
|
||||||
|
FeatureInstructionOrder::WorkflowPolicy,
|
||||||
|
"Typed Ticket workflow guidance",
|
||||||
|
)
|
||||||
|
.expect("static Ticket workflow instruction declaration is valid")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct TicketFeatureAccess {
|
pub struct TicketFeatureAccess {
|
||||||
|
|
@ -326,7 +339,8 @@ impl TicketFeature {
|
||||||
impl FeatureModule for TicketFeature {
|
impl FeatureModule for TicketFeature {
|
||||||
fn descriptor(&self) -> FeatureDescriptor {
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME)
|
let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME)
|
||||||
.with_description(FEATURE_DESCRIPTION);
|
.with_description(FEATURE_DESCRIPTION)
|
||||||
|
.with_instruction(ticket_workflow_instruction());
|
||||||
let enabled_tool_names = self.enabled_tool_names();
|
let enabled_tool_names = self.enabled_tool_names();
|
||||||
for name in enabled_tool_names {
|
for name in enabled_tool_names {
|
||||||
descriptor = descriptor.with_tool(ToolDeclaration::new(
|
descriptor = descriptor.with_tool(ToolDeclaration::new(
|
||||||
|
|
@ -349,6 +363,11 @@ impl FeatureModule for TicketFeature {
|
||||||
let Some(backend) = self.tool_backend(context) else {
|
let Some(backend) = self.tool_backend(context) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
context
|
||||||
|
.instructions()
|
||||||
|
.register(FeatureInstructionContribution::new(
|
||||||
|
ticket_workflow_instruction(),
|
||||||
|
))?;
|
||||||
let allowed_tool_names = self.enabled_tool_names();
|
let allowed_tool_names = self.enabled_tool_names();
|
||||||
let mut tools = context.tools();
|
let mut tools = context.tools();
|
||||||
for definition in ticket_tools(backend) {
|
for definition in ticket_tools(backend) {
|
||||||
|
|
|
||||||
|
|
@ -669,6 +669,7 @@ impl FeatureModule for PluginToolFeature {
|
||||||
}),
|
}),
|
||||||
tools: Vec::new(),
|
tools: Vec::new(),
|
||||||
hooks: Vec::new(),
|
hooks: Vec::new(),
|
||||||
|
instructions: Vec::new(),
|
||||||
background_tasks: Vec::new(),
|
background_tasks: Vec::new(),
|
||||||
provides_services: Vec::new(),
|
provides_services: Vec::new(),
|
||||||
requires_services: Vec::new(),
|
requires_services: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use minijinja::value::Value;
|
||||||
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
|
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
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};
|
use crate::prompt::loader::{LoaderError, PromptLoader, PromptRef};
|
||||||
|
|
||||||
|
|
@ -120,11 +121,12 @@ impl SystemPromptTemplate {
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
||||||
append_trailing_section(
|
append_trailing_section(
|
||||||
&body,
|
&body,
|
||||||
|
&self.env,
|
||||||
|
ctx,
|
||||||
ctx.prompts,
|
ctx.prompts,
|
||||||
ctx.scope,
|
ctx.scope,
|
||||||
ctx.agents_md.as_deref(),
|
ctx.agents_md.as_deref(),
|
||||||
ctx.resident_summary,
|
ctx.resident_summary,
|
||||||
ToolCapabilities::from_tool_names(&ctx.tool_names),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +151,7 @@ pub struct SystemPromptContext<'a> {
|
||||||
pub language: &'a str,
|
pub language: &'a str,
|
||||||
pub scope: &'a Scope,
|
pub scope: &'a Scope,
|
||||||
pub tool_names: Vec<String>,
|
pub tool_names: Vec<String>,
|
||||||
|
pub feature_instructions: &'a [FeatureInstructionDeclaration],
|
||||||
/// Project-level instructions read from the nearest `AGENTS.md`.
|
/// Project-level instructions read from the nearest `AGENTS.md`.
|
||||||
/// Not visible from the template; consumed by the trailing-section
|
/// Not visible from the template; consumed by the trailing-section
|
||||||
/// formatter in [`SystemPromptTemplate::render`].
|
/// formatter in [`SystemPromptTemplate::render`].
|
||||||
|
|
@ -209,7 +212,6 @@ struct ToolCapabilities {
|
||||||
worker_stop: bool,
|
worker_stop: bool,
|
||||||
worker_list: bool,
|
worker_list: bool,
|
||||||
worker_restore: bool,
|
worker_restore: bool,
|
||||||
ticket_any: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolCapabilities {
|
impl ToolCapabilities {
|
||||||
|
|
@ -226,7 +228,6 @@ impl ToolCapabilities {
|
||||||
"StopWorker" => capabilities.worker_stop = true,
|
"StopWorker" => capabilities.worker_stop = true,
|
||||||
"ListWorkers" => capabilities.worker_list = true,
|
"ListWorkers" => capabilities.worker_list = true,
|
||||||
"RestoreWorker" => capabilities.worker_restore = true,
|
"RestoreWorker" => capabilities.worker_restore = true,
|
||||||
name if name.starts_with("Ticket") => capabilities.ticket_any = true,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,7 +270,6 @@ impl ToolCapabilities {
|
||||||
);
|
);
|
||||||
map.insert("memory_mutation", Value::from(self.memory_mutation()));
|
map.insert("memory_mutation", Value::from(self.memory_mutation()));
|
||||||
map.insert("worker_management", Value::from(self.worker_management()));
|
map.insert("worker_management", Value::from(self.worker_management()));
|
||||||
map.insert("ticket_any", Value::from(self.ticket_any));
|
|
||||||
Value::from(map)
|
Value::from(map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -282,11 +282,12 @@ 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<'_>,
|
||||||
prompts: &PromptCatalog,
|
prompts: &PromptCatalog,
|
||||||
scope: &Scope,
|
scope: &Scope,
|
||||||
agents_md: Option<&str>,
|
agents_md: Option<&str>,
|
||||||
resident_summary: Option<&str>,
|
resident_summary: Option<&str>,
|
||||||
tool_capabilities: ToolCapabilities,
|
|
||||||
) -> Result<String, SystemPromptError> {
|
) -> Result<String, SystemPromptError> {
|
||||||
let mut out = String::with_capacity(body.len() + 256);
|
let mut out = String::with_capacity(body.len() + 256);
|
||||||
out.push_str(body);
|
out.push_str(body);
|
||||||
|
|
@ -313,12 +314,20 @@ fn append_trailing_section(
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tool_capabilities.worker_management() {
|
for instruction in dedupe_instruction_contributions(ctx.feature_instructions.iter().cloned()) {
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
let section = prompts.worker_orchestration_guidance_section()?;
|
let template = env
|
||||||
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
|
.get_template(&instruction.prompt_ref)
|
||||||
|
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
||||||
|
let section = template
|
||||||
|
.render(ctx.to_minijinja_value())
|
||||||
|
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
||||||
|
let section = section.trim_end_matches(&['\n', ' '][..]);
|
||||||
|
if !section.trim().is_empty() {
|
||||||
|
out.push_str(section);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Canonicalise the tail so the emitted prompt has a single form
|
// Canonicalise the tail so the emitted prompt has a single form
|
||||||
// regardless of how individual templates chose to end.
|
// regardless of how individual templates chose to end.
|
||||||
while out.ends_with('\n') || out.ends_with(' ') {
|
while out.ends_with('\n') || out.ends_with(' ') {
|
||||||
|
|
@ -369,6 +378,7 @@ mod tests {
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: tools,
|
tool_names: tools,
|
||||||
|
feature_instructions: &[],
|
||||||
agents_md,
|
agents_md,
|
||||||
resident_summary: None,
|
resident_summary: None,
|
||||||
prompts: test_prompts(),
|
prompts: test_prompts(),
|
||||||
|
|
@ -386,25 +396,13 @@ mod tests {
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: Vec::new(),
|
tool_names: Vec::new(),
|
||||||
|
feature_instructions: &[],
|
||||||
agents_md: None,
|
agents_md: None,
|
||||||
resident_summary: summary,
|
resident_summary: summary,
|
||||||
prompts: test_prompts(),
|
prompts: test_prompts(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ctx_with_resident<'a>(cwd: &'a Path, scope: &'a Scope) -> SystemPromptContext<'a> {
|
|
||||||
SystemPromptContext {
|
|
||||||
now: fixed_now(),
|
|
||||||
cwd: cwd.display().to_string().into(),
|
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
|
||||||
scope,
|
|
||||||
tool_names: Vec::new(),
|
|
||||||
agents_md: None,
|
|
||||||
resident_summary: None,
|
|
||||||
prompts: test_prompts(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn memory_tool_names() -> Vec<String> {
|
fn memory_tool_names() -> Vec<String> {
|
||||||
["MemoryQuery", "MemoryReadDocument", "MemoryUpdateDocument"]
|
["MemoryQuery", "MemoryReadDocument", "MemoryUpdateDocument"]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -412,25 +410,24 @@ mod tests {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_management_tool_names() -> Vec<String> {
|
fn ticket_instruction() -> FeatureInstructionDeclaration {
|
||||||
[
|
FeatureInstructionDeclaration::new(
|
||||||
"SpawnWorker",
|
crate::feature::FeatureInstructionId::builtin("ticket.workflow"),
|
||||||
"SendToWorker",
|
"$yoi/common/tickets",
|
||||||
"ReadWorkerOutput",
|
crate::feature::FeatureInstructionOrder::WorkflowPolicy,
|
||||||
"StopWorker",
|
"Ticket workflow guidance",
|
||||||
"ListWorkers",
|
)
|
||||||
"RestoreWorker",
|
.unwrap()
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ticket_tool_names() -> Vec<String> {
|
fn worker_orchestration_instruction() -> FeatureInstructionDeclaration {
|
||||||
["TicketList", "TicketShow", "TicketComment"]
|
FeatureInstructionDeclaration::new(
|
||||||
.into_iter()
|
crate::feature::FeatureInstructionId::builtin("worker.orchestration"),
|
||||||
.map(String::from)
|
"$yoi/common/worker-orchestration",
|
||||||
.collect()
|
crate::feature::FeatureInstructionOrder::OrchestrationPolicy,
|
||||||
|
"Worker orchestration guidance",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lazily-initialised builtin catalog shared across system-prompt
|
/// Lazily-initialised builtin catalog shared across system-prompt
|
||||||
|
|
@ -499,14 +496,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_guidance_is_included_for_typed_ticket_tools() {
|
fn ticket_guidance_is_included_for_ticket_feature_instruction() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptLoader::builtins_only();
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let scope = build_scope(dir.path());
|
let scope = build_scope(dir.path());
|
||||||
let rendered = tmpl
|
let instructions = [ticket_instruction()];
|
||||||
.render(&ctx(dir.path(), &scope, ticket_tool_names(), None))
|
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
||||||
.unwrap();
|
ctx.feature_instructions = &instructions;
|
||||||
|
let rendered = tmpl.render(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(rendered.contains("## Ticket workflow"));
|
assert!(rendered.contains("## Ticket workflow"));
|
||||||
assert!(rendered.contains("available typed Ticket tools as the authority"));
|
assert!(rendered.contains("available typed Ticket tools as the authority"));
|
||||||
|
|
@ -514,6 +512,21 @@ mod tests {
|
||||||
assert!(rendered.contains("Distinguish implementation completion"));
|
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]
|
#[test]
|
||||||
fn ticket_guidance_is_omitted_without_ticket_tools() {
|
fn ticket_guidance_is_omitted_without_ticket_tools() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptLoader::builtins_only();
|
||||||
|
|
@ -534,17 +547,18 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_role_instructions_include_common_ticket_guidance() {
|
fn ticket_role_instructions_include_feature_ticket_guidance() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptLoader::builtins_only();
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let scope = build_scope(dir.path());
|
let scope = build_scope(dir.path());
|
||||||
|
let instructions = [ticket_instruction()];
|
||||||
|
|
||||||
for role in ["intake", "orchestrator", "coder", "reviewer"] {
|
for role in ["intake", "orchestrator", "coder", "reviewer"] {
|
||||||
let tmpl =
|
let tmpl =
|
||||||
SystemPromptTemplate::parse(&format!("$yoi/role/{role}"), loader.clone()).unwrap();
|
SystemPromptTemplate::parse(&format!("$yoi/role/{role}"), loader.clone()).unwrap();
|
||||||
let rendered = tmpl
|
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
||||||
.render(&ctx(dir.path(), &scope, ticket_tool_names(), None))
|
ctx.feature_instructions = &instructions;
|
||||||
.unwrap();
|
let rendered = tmpl.render(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(rendered.contains("## Ticket workflow"), "role: {role}");
|
assert!(rendered.contains("## Ticket workflow"), "role: {role}");
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -578,19 +592,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn worker_orchestration_guidance_is_included_for_worker_management_tools() {
|
fn worker_orchestration_guidance_is_included_for_feature_instruction() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptLoader::builtins_only();
|
||||||
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
let tmpl = SystemPromptTemplate::parse("$yoi/default", loader).unwrap();
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let scope = build_scope(dir.path());
|
let scope = build_scope(dir.path());
|
||||||
let rendered = tmpl
|
let instructions = [worker_orchestration_instruction()];
|
||||||
.render(&ctx(
|
let mut ctx = ctx(dir.path(), &scope, vec!["Read".into()], None);
|
||||||
dir.path(),
|
ctx.feature_instructions = &instructions;
|
||||||
&scope,
|
let rendered = tmpl.render(&ctx).unwrap();
|
||||||
worker_management_tool_names(),
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(rendered.contains("## Worker orchestration"));
|
assert!(rendered.contains("## Worker orchestration"));
|
||||||
assert!(rendered.contains("spawned Worker notifications are background signals"));
|
assert!(rendered.contains("spawned Worker notifications are background signals"));
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ use crate::feature::builtin::memory::WorkspaceMemoryBackendError;
|
||||||
use crate::feature::builtin::{
|
use crate::feature::builtin::{
|
||||||
SessionExploreFeature, SessionExploreState, TaskFeature, render_extract_input,
|
SessionExploreFeature, SessionExploreState, TaskFeature, render_extract_input,
|
||||||
};
|
};
|
||||||
use crate::feature::{FeatureRegistryBuilder, FeatureRegistryInstallReport};
|
use crate::feature::{
|
||||||
|
FeatureInstructionDeclaration, FeatureInstructionId, FeatureInstructionOrder,
|
||||||
|
FeatureRegistryBuilder, FeatureRegistryInstallReport, dedupe_instruction_contributions,
|
||||||
|
};
|
||||||
use crate::hook::{
|
use crate::hook::{
|
||||||
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
|
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
|
||||||
PreToolCall,
|
PreToolCall,
|
||||||
|
|
@ -44,6 +47,18 @@ use crate::internal_worker::{InternalWorkerSpec, run_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_PROMPT_REF: &str = "$yoi/common/worker-orchestration";
|
||||||
|
|
||||||
|
fn worker_orchestration_instruction() -> FeatureInstructionDeclaration {
|
||||||
|
FeatureInstructionDeclaration::new(
|
||||||
|
FeatureInstructionId::builtin(WORKER_ORCHESTRATION_INSTRUCTION_ID),
|
||||||
|
WORKER_ORCHESTRATION_PROMPT_REF,
|
||||||
|
FeatureInstructionOrder::OrchestrationPolicy,
|
||||||
|
"Worker orchestration guidance",
|
||||||
|
)
|
||||||
|
.expect("static Worker orchestration instruction declaration is valid")
|
||||||
|
}
|
||||||
use crate::ipc::alerter::Alerter;
|
use crate::ipc::alerter::Alerter;
|
||||||
use crate::ipc::interceptor::WorkerInterceptor;
|
use crate::ipc::interceptor::WorkerInterceptor;
|
||||||
use crate::ipc::notify_buffer::NotifyBuffer;
|
use crate::ipc::notify_buffer::NotifyBuffer;
|
||||||
|
|
@ -459,6 +474,10 @@ pub struct Worker<C: LlmClient, St: Store> {
|
||||||
/// `Some` until `ensure_system_prompt_materialized` renders it once,
|
/// `Some` until `ensure_system_prompt_materialized` renders it once,
|
||||||
/// then `None` forever — including after compaction.
|
/// then `None` forever — including after compaction.
|
||||||
system_prompt_template: Option<SystemPromptTemplate>,
|
system_prompt_template: Option<SystemPromptTemplate>,
|
||||||
|
/// Mandatory prompt sections contributed by enabled Worker features.
|
||||||
|
/// These are appended by Rust-owned prompt assembly so authored top-level
|
||||||
|
/// templates cannot accidentally omit feature workflow guidance.
|
||||||
|
feature_instructions: Vec<FeatureInstructionDeclaration>,
|
||||||
/// User-facing notification sink attached by the Controller at
|
/// User-facing notification sink attached by the Controller at
|
||||||
/// spawn time. `None` in tests / direct `Worker::new` usage.
|
/// spawn time. `None` in tests / direct `Worker::new` usage.
|
||||||
alerter: Option<Alerter>,
|
alerter: Option<Alerter>,
|
||||||
|
|
@ -604,6 +623,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Worker<C, St>
|
||||||
tracker: None,
|
tracker: None,
|
||||||
task_feature: self.task_feature.clone(),
|
task_feature: self.task_feature.clone(),
|
||||||
system_prompt_template: None,
|
system_prompt_template: None,
|
||||||
|
feature_instructions: self.feature_instructions.clone(),
|
||||||
alerter: self.alerter.clone(),
|
alerter: self.alerter.clone(),
|
||||||
event_tx: self.event_tx.clone(),
|
event_tx: self.event_tx.clone(),
|
||||||
in_flight: self.in_flight.clone(),
|
in_flight: self.in_flight.clone(),
|
||||||
|
|
@ -797,6 +817,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
tracker: None,
|
tracker: None,
|
||||||
task_feature: TaskFeature::new(),
|
task_feature: TaskFeature::new(),
|
||||||
system_prompt_template: None,
|
system_prompt_template: None,
|
||||||
|
feature_instructions: Vec::new(),
|
||||||
alerter: None,
|
alerter: None,
|
||||||
event_tx: None,
|
event_tx: None,
|
||||||
in_flight: None,
|
in_flight: None,
|
||||||
|
|
@ -830,6 +851,16 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
self.system_prompt_template = Some(template);
|
self.system_prompt_template = Some(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_feature_instruction(&mut self, instruction: FeatureInstructionDeclaration) {
|
||||||
|
let mut instructions = self.feature_instructions.clone();
|
||||||
|
instructions.push(instruction);
|
||||||
|
self.feature_instructions = dedupe_instruction_contributions(instructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_worker_orchestration_instruction(&mut self) {
|
||||||
|
self.register_feature_instruction(worker_orchestration_instruction());
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle all resident sections in the system prompt.
|
/// Toggle all resident sections in the system prompt.
|
||||||
///
|
///
|
||||||
/// Default `true`: normal Workers may expose each resident section according
|
/// Default `true`: normal Workers may expose each resident section according
|
||||||
|
|
@ -1022,7 +1053,11 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
registry: FeatureRegistryBuilder,
|
registry: FeatureRegistryBuilder,
|
||||||
) -> FeatureRegistryInstallReport {
|
) -> FeatureRegistryInstallReport {
|
||||||
let worker = self.engine.as_mut().expect("worker taken during run");
|
let worker = self.engine.as_mut().expect("worker taken during run");
|
||||||
registry.install_into_engine(worker, &mut self.hook_builder)
|
let report = registry.install_into_engine(worker, &mut self.hook_builder);
|
||||||
|
for instruction in report.installed_instruction_contributions() {
|
||||||
|
self.register_feature_instruction(instruction);
|
||||||
|
}
|
||||||
|
report
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reference to the store.
|
/// Reference to the store.
|
||||||
|
|
@ -1528,6 +1563,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
language: worker_language,
|
language: worker_language,
|
||||||
scope: &scope_snapshot,
|
scope: &scope_snapshot,
|
||||||
tool_names,
|
tool_names,
|
||||||
|
feature_instructions: &self.feature_instructions,
|
||||||
agents_md: agents_md_read.and_then(|read| read.body),
|
agents_md: agents_md_read.and_then(|read| read.body),
|
||||||
resident_summary: resident_summary.as_deref(),
|
resident_summary: resident_summary.as_deref(),
|
||||||
prompts: &self.prompts,
|
prompts: &self.prompts,
|
||||||
|
|
@ -3608,6 +3644,7 @@ where
|
||||||
tracker: None,
|
tracker: None,
|
||||||
task_feature: TaskFeature::new(),
|
task_feature: TaskFeature::new(),
|
||||||
system_prompt_template: common.system_prompt_template,
|
system_prompt_template: common.system_prompt_template,
|
||||||
|
feature_instructions: common.feature_instructions,
|
||||||
alerter: None,
|
alerter: None,
|
||||||
event_tx: None,
|
event_tx: None,
|
||||||
in_flight: None,
|
in_flight: None,
|
||||||
|
|
@ -3713,6 +3750,7 @@ where
|
||||||
tracker: None,
|
tracker: None,
|
||||||
task_feature: TaskFeature::new(),
|
task_feature: TaskFeature::new(),
|
||||||
system_prompt_template: common.system_prompt_template,
|
system_prompt_template: common.system_prompt_template,
|
||||||
|
feature_instructions: common.feature_instructions,
|
||||||
alerter: None,
|
alerter: None,
|
||||||
event_tx: None,
|
event_tx: None,
|
||||||
in_flight: None,
|
in_flight: None,
|
||||||
|
|
@ -4003,6 +4041,7 @@ where
|
||||||
// Restore replays the saved system_prompt verbatim — no
|
// Restore replays the saved system_prompt verbatim — no
|
||||||
// template re-render on resume.
|
// template re-render on resume.
|
||||||
system_prompt_template: None,
|
system_prompt_template: None,
|
||||||
|
feature_instructions: common.feature_instructions,
|
||||||
alerter: None,
|
alerter: None,
|
||||||
event_tx: None,
|
event_tx: None,
|
||||||
in_flight: None,
|
in_flight: None,
|
||||||
|
|
@ -4666,6 +4705,7 @@ struct WorkerCommon {
|
||||||
client: Box<dyn LlmClient>,
|
client: Box<dyn LlmClient>,
|
||||||
prompts: Arc<PromptCatalog>,
|
prompts: Arc<PromptCatalog>,
|
||||||
system_prompt_template: Option<SystemPromptTemplate>,
|
system_prompt_template: Option<SystemPromptTemplate>,
|
||||||
|
feature_instructions: Vec<FeatureInstructionDeclaration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restored_child_reachable(child: &WorkerSpawnedChild) -> bool {
|
async fn restored_child_reachable(child: &WorkerSpawnedChild) -> bool {
|
||||||
|
|
@ -4823,6 +4863,7 @@ fn prepare_worker_common_from_scope(
|
||||||
client,
|
client,
|
||||||
prompts,
|
prompts,
|
||||||
system_prompt_template,
|
system_prompt_template,
|
||||||
|
feature_instructions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
{% if tool_capabilities.ticket_any %}
|
|
||||||
## Ticket workflow
|
## Ticket workflow
|
||||||
|
|
||||||
Use the available typed Ticket tools as the authority for Ticket reads and mutations. Do not invoke a Ticket CLI or edit backend storage directly as an alternative implementation of those tools.
|
Use the available typed Ticket tools as the authority for Ticket reads and mutations. Do not invoke a Ticket CLI or edit backend storage directly as an alternative implementation of those tools.
|
||||||
|
|
@ -8,4 +7,3 @@ Read the relevant Ticket before making implementation, routing, review, state, o
|
||||||
Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements.
|
Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements.
|
||||||
|
|
||||||
Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker.
|
Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker.
|
||||||
{% endif %}
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ Stay precise, edit code directly when asked, and avoid speculative refactoring.
|
||||||
|
|
||||||
{% include "common/tool-usage" %}
|
{% include "common/tool-usage" %}
|
||||||
|
|
||||||
{% include "common/tickets" %}
|
|
||||||
|
|
||||||
{% include "common/language" %}
|
{% include "common/language" %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
You are the Ticket Coder role.
|
You are the Ticket Coder role.
|
||||||
|
|
||||||
{% include "$yoi/common/tickets" %}
|
|
||||||
|
|
||||||
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Implement only within the delegated worktree/branch and authority scope. Treat the Ticket, intent packet, binding decisions/invariants, implementation latitude, validation expectations, and report expectations as the contract.
|
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Implement only within the delegated worktree/branch and authority scope. Treat the Ticket, intent packet, binding decisions/invariants, implementation latitude, validation expectations, and report expectations as the contract.
|
||||||
|
|
||||||
Choose local implementation tactics within that contract. Escalate to the Orchestrator instead of expanding scope when design, permission, dependency, prompt-boundary, or Ticket-boundary questions appear. Do not merge, push, close Tickets, delete worktrees, or create generated memory/local/runtime/log/lock/cache/socket/secret-like `.yoi` state.
|
Choose local implementation tactics within that contract. Escalate to the Orchestrator instead of expanding scope when design, permission, dependency, prompt-boundary, or Ticket-boundary questions appear. Do not merge, push, close Tickets, delete worktrees, or create generated memory/local/runtime/log/lock/cache/socket/secret-like `.yoi` state.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
You are the Ticket Intake role.
|
You are the Ticket Intake role.
|
||||||
|
|
||||||
{% include "$yoi/common/tickets" %}
|
|
||||||
|
|
||||||
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Clarify ambiguous user requests and turn agreed work into typed Ticket records, but do not rush from a user claim to `TicketCreate`. Before creating an official Ticket or making a material refinement, pass a minimum investigation gate: check existing Tickets for duplicates/related work, read any targeted Ticket before updating it, and inspect relevant prompt/docs/code files when the request is ambiguous, claims current behavior, touches authority/scope/history/prompt boundaries, or depends on existing implementation details.
|
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Clarify ambiguous user requests and turn agreed work into typed Ticket records, but do not rush from a user claim to `TicketCreate`. Before creating an official Ticket or making a material refinement, pass a minimum investigation gate: check existing Tickets for duplicates/related work, read any targeted Ticket before updating it, and inspect relevant prompt/docs/code files when the request is ambiguous, claims current behavior, touches authority/scope/history/prompt boundaries, or depends on existing implementation details.
|
||||||
|
|
||||||
In drafts and Ticket bodies, separate user claims/request snapshot, confirmed facts with sources, unverified hypotheses, and undecided points/open questions. Do not save all user claims as requirements or acceptance criteria. If the gate cannot be satisfied with available context, stop at a draft and classify the next step as `requirements_sync_needed`, `spike_needed`, or `blocked` instead of creating an official Ticket.
|
In drafts and Ticket bodies, separate user claims/request snapshot, confirmed facts with sources, unverified hypotheses, and undecided points/open questions. Do not save all user claims as requirements or acceptance criteria. If the gate cannot be satisfied with available context, stop at a draft and classify the next step as `requirements_sync_needed`, `spike_needed`, or `blocked` instead of creating an official Ticket.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
You are the Ticket Orchestrator role.
|
You are the Ticket Orchestrator role.
|
||||||
|
|
||||||
{% include "$yoi/common/tickets" %}
|
|
||||||
|
|
||||||
Keep durable orchestration behavior here and treat the first committed user message as concrete Ticket/action context only. Use typed Ticket tools and current repository state as authority. Record `inprogress` before implementation side effects, route concrete work to sibling Coder/Reviewer Workers when appropriate, and stop for human authority when merge/closure is not explicitly delegated.
|
Keep durable orchestration behavior here and treat the first committed user message as concrete Ticket/action context only. Use typed Ticket tools and current repository state as authority. Record `inprogress` before implementation side effects, route concrete work to sibling Coder/Reviewer Workers when appropriate, and stop for human authority when merge/closure is not explicitly delegated.
|
||||||
|
|
||||||
Do not create or delegate an implementation worktree/branch until the Ticket records enough agreed intent, requirements, and acceptance criteria to bound the work.
|
Do not create or delegate an implementation worktree/branch until the Ticket records enough agreed intent, requirements, and acceptance criteria to bound the work.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
You are the Ticket Reviewer role.
|
You are the Ticket Reviewer role.
|
||||||
|
|
||||||
{% include "$yoi/common/tickets" %}
|
|
||||||
|
|
||||||
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Review the implementation against the Ticket intent, binding decisions/invariants, acceptance criteria, and project design boundaries. Prefer read-only inspection and focused validation; do not merge, close, clean up worktrees, or take over implementation unless explicitly asked.
|
Keep role behavior here and treat the first committed user message as concrete Ticket/action context only. Review the implementation against the Ticket intent, binding decisions/invariants, acceptance criteria, and project design boundaries. Prefer read-only inspection and focused validation; do not merge, close, clean up worktrees, or take over implementation unless explicitly asked.
|
||||||
|
|
||||||
Report clear approve/request-changes evidence with risks, validation performed, and any unresolved requirement or design-boundary concern. When a workflow is invoked, follow that workflow as the procedural authority for reviewer handoff and report shape.
|
Report clear approve/request-changes evidence with risks, validation performed, and any unresolved requirement or design-boundary concern. When a workflow is invoked, follow that workflow as the procedural authority for reviewer handoff and report shape.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user