prompt: preserve feature instruction order

This commit is contained in:
Keisuke Hirata 2026-07-28 22:00:16 +09:00
parent 44e7014d83
commit 450e0cddbd
No known key found for this signature in database
4 changed files with 17 additions and 51 deletions

View File

@ -11,7 +11,7 @@
//! [`crate::hook::HookRegistryBuilder`], and provider output is represented as
//! ordinary feature reports/diagnostics instead of a separate authority layer.
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
@ -340,18 +340,10 @@ impl fmt::Display for FeatureInstructionId {
}
}
#[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,
}
@ -359,7 +351,6 @@ 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();
@ -371,7 +362,6 @@ impl FeatureInstructionDeclaration {
Ok(Self {
id,
prompt_ref,
order,
description: description.into(),
})
}
@ -1396,13 +1386,14 @@ impl FeatureRegistryInstallReport {
pub fn dedupe_instruction_contributions(
instructions: impl IntoIterator<Item = FeatureInstructionDeclaration>,
) -> Vec<FeatureInstructionDeclaration> {
let mut by_id = BTreeMap::new();
let mut seen = HashSet::new();
let mut deduped = Vec::new();
for instruction in instructions {
by_id.entry(instruction.id.clone()).or_insert(instruction);
if seen.insert(instruction.id.clone()) {
deduped.push(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
deduped
}
/// Builder/installer for enabled feature modules.
@ -1730,15 +1721,10 @@ mod tests {
}
}
fn instruction(
id: &'static str,
prompt_ref: &'static str,
order: FeatureInstructionOrder,
) -> FeatureInstructionDeclaration {
fn instruction(id: &'static str, prompt_ref: &'static str) -> FeatureInstructionDeclaration {
FeatureInstructionDeclaration::new(
FeatureInstructionId::builtin(id),
prompt_ref,
order,
"test instruction",
)
.unwrap()
@ -1771,38 +1757,22 @@ mod tests {
}
#[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,
);
fn instruction_contributions_are_deduped_in_registration_order() {
let workflow = instruction("workflow", "$yoi/common/tickets");
let orchestration = instruction("orchestration", "$yoi/common/worker-orchestration");
let contributions = dedupe_instruction_contributions([
workflow.clone(),
orchestration.clone(),
workflow.clone(),
]);
assert_eq!(contributions, vec![orchestration, workflow]);
assert_eq!(contributions, vec![workflow, orchestration]);
}
#[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 declared = instruction("declared", "$yoi/common/tickets");
let undeclared = instruction("undeclared", "$yoi/common/tickets");
let descriptor =
FeatureDescriptor::builtin("instruction", "Instruction").with_instruction(declared);
let mut hook_builder = HookRegistryBuilder::default();

View File

@ -20,7 +20,7 @@ use ticket::{
use crate::feature::{
FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError,
FeatureInstructionContribution, FeatureInstructionDeclaration, FeatureInstructionId,
FeatureInstructionOrder, FeatureModule, ToolContribution, ToolDeclaration,
FeatureModule, ToolContribution, ToolDeclaration,
};
const FEATURE_ID: &str = "ticket";
@ -34,7 +34,6 @@ 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")

View File

@ -414,7 +414,6 @@ mod tests {
FeatureInstructionDeclaration::new(
crate::feature::FeatureInstructionId::builtin("ticket.workflow"),
"$yoi/common/tickets",
crate::feature::FeatureInstructionOrder::WorkflowPolicy,
"Ticket workflow guidance",
)
.unwrap()
@ -424,7 +423,6 @@ mod tests {
FeatureInstructionDeclaration::new(
crate::feature::FeatureInstructionId::builtin("worker.orchestration"),
"$yoi/common/worker-orchestration",
crate::feature::FeatureInstructionOrder::OrchestrationPolicy,
"Worker orchestration guidance",
)
.unwrap()

View File

@ -35,8 +35,8 @@ use crate::feature::builtin::{
SessionExploreFeature, SessionExploreState, TaskFeature, render_extract_input,
};
use crate::feature::{
FeatureInstructionDeclaration, FeatureInstructionId, FeatureInstructionOrder,
FeatureRegistryBuilder, FeatureRegistryInstallReport, dedupe_instruction_contributions,
FeatureInstructionDeclaration, FeatureInstructionId, FeatureRegistryBuilder,
FeatureRegistryInstallReport, dedupe_instruction_contributions,
};
use crate::hook::{
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
@ -54,7 +54,6 @@ 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")