ticket: guide Ticket tool language universally
This commit is contained in:
parent
76d358e824
commit
92c4dee71a
|
|
@ -12,7 +12,7 @@ use ticket::{
|
||||||
tool::{
|
tool::{
|
||||||
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
||||||
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||||
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tools,
|
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tool_description, ticket_tools,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,7 +178,10 @@ impl FeatureModule for TicketFeature {
|
||||||
));
|
));
|
||||||
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(*name, tool_description(name)));
|
descriptor = descriptor.with_tool(ToolDeclaration::new(
|
||||||
|
*name,
|
||||||
|
ticket_tool_description(name, self.record_language.as_deref()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
descriptor
|
descriptor
|
||||||
}
|
}
|
||||||
|
|
@ -227,37 +230,6 @@ impl FeatureModule for TicketFeature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_description(name: &str) -> &'static str {
|
|
||||||
match name {
|
|
||||||
"TicketCreate" => "Create a Ticket through the typed local Ticket backend.",
|
|
||||||
"TicketList" => {
|
|
||||||
"List Tickets as a lightweight bounded overview for id selection; use TicketShow before decisions."
|
|
||||||
}
|
|
||||||
"TicketShow" => {
|
|
||||||
"Show one Ticket through the typed local Ticket backend as the detailed authority."
|
|
||||||
}
|
|
||||||
"TicketComment" => {
|
|
||||||
"Append a comment/plan/decision/implementation_report event to a Ticket."
|
|
||||||
}
|
|
||||||
"TicketReview" => "Append an approve/request_changes review event to a Ticket.",
|
|
||||||
"TicketIntakeReady" => {
|
|
||||||
"Mark an intake Ticket ready and append the typed intake summary/state transition events."
|
|
||||||
}
|
|
||||||
"TicketWorkflowState" => {
|
|
||||||
"Transition Ticket state; queued -> inprogress is the accepted implementation start, so implementation side effects should happen only after that transition is accepted and recorded."
|
|
||||||
}
|
|
||||||
"TicketClose" => "Close a Ticket with a resolution through the typed local Ticket backend.",
|
|
||||||
"TicketOrchestrationPlanRecord" => {
|
|
||||||
"Append a durable typed Ticket orchestration plan record without changing state or starting work."
|
|
||||||
}
|
|
||||||
"TicketOrchestrationPlanQuery" => {
|
|
||||||
"Query durable Ticket orchestration plan records by Ticket and/or relation kind."
|
|
||||||
}
|
|
||||||
"TicketDoctor" => "Run typed local Ticket backend consistency checks.",
|
|
||||||
_ => "Typed Ticket backend tool.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ticket_tools_feature(workspace: impl AsRef<Path>) -> TicketFeature {
|
pub fn ticket_tools_feature(workspace: impl AsRef<Path>) -> TicketFeature {
|
||||||
TicketFeature::for_workspace(workspace)
|
TicketFeature::for_workspace(workspace)
|
||||||
}
|
}
|
||||||
|
|
@ -298,6 +270,19 @@ mod tests {
|
||||||
std::fs::write(yoi_dir.join("ticket.config.toml"), content).unwrap();
|
std::fs::write(yoi_dir.join("ticket.config.toml"), content).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pending_tool_description(
|
||||||
|
pending_tools: &[llm_worker::tool::ToolDefinition],
|
||||||
|
name: &str,
|
||||||
|
) -> String {
|
||||||
|
pending_tools
|
||||||
|
.iter()
|
||||||
|
.find_map(|definition| {
|
||||||
|
let (meta, _) = definition();
|
||||||
|
(meta.name == name).then_some(meta.description)
|
||||||
|
})
|
||||||
|
.expect("tool exists")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn descriptor_declares_ticket_tools_and_backend_authority() {
|
fn descriptor_declares_ticket_tools_and_backend_authority() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -407,6 +392,45 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_only_companion_style_context_exposes_ticket_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(
|
||||||
|
temp.path(),
|
||||||
|
r#"
|
||||||
|
[ticket]
|
||||||
|
language = "Japanese"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||||
|
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly);
|
||||||
|
let descriptor = feature.descriptor();
|
||||||
|
let descriptor_description = descriptor
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.find(|tool| tool.name == "TicketShow")
|
||||||
|
.expect("TicketShow declared")
|
||||||
|
.description
|
||||||
|
.clone();
|
||||||
|
assert!(descriptor_description.contains("Ticket record language: Japanese"));
|
||||||
|
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let mut hooks = HookRegistryBuilder::default();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(feature)
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||||
|
|
||||||
|
assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len());
|
||||||
|
assert_eq!(
|
||||||
|
report.reports[0].installed_tools,
|
||||||
|
TICKET_READ_ONLY_TOOL_NAMES
|
||||||
|
);
|
||||||
|
let description = pending_tool_description(&pending_tools, "TicketShow");
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("Preserve protocol literals"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lifecycle_installation_exposes_lifecycle_tools() {
|
fn lifecycle_installation_exposes_lifecycle_tools() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -444,6 +468,35 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_ticket_role_style_context_exposes_ticket_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(
|
||||||
|
temp.path(),
|
||||||
|
r#"
|
||||||
|
[ticket]
|
||||||
|
language = "Japanese"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let mut hooks = HookRegistryBuilder::default();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(ticket_tools_feature_with_access(
|
||||||
|
temp.path(),
|
||||||
|
TicketFeatureAccess::Lifecycle,
|
||||||
|
))
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||||
|
|
||||||
|
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len());
|
||||||
|
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES);
|
||||||
|
let description = pending_tool_description(&pending_tools, "TicketComment");
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("durable Ticket record and Ticket tool body text"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("memory.language"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn installs_ticket_tools_when_default_root_is_usable() {
|
fn installs_ticket_tools_when_default_root_is_usable() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,41 @@ explicit state decisions.";
|
||||||
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
||||||
diagnostics through the typed backend without shelling out to external commands.";
|
diagnostics through the typed backend without shelling out to external commands.";
|
||||||
|
|
||||||
|
fn base_tool_description(name: &str) -> &'static str {
|
||||||
|
match name {
|
||||||
|
"TicketCreate" => CREATE_DESCRIPTION,
|
||||||
|
"TicketList" => LIST_DESCRIPTION,
|
||||||
|
"TicketShow" => SHOW_DESCRIPTION,
|
||||||
|
"TicketComment" => COMMENT_DESCRIPTION,
|
||||||
|
"TicketReview" => REVIEW_DESCRIPTION,
|
||||||
|
"TicketIntakeReady" => INTAKE_READY_DESCRIPTION,
|
||||||
|
"TicketWorkflowState" => WORKFLOW_STATE_DESCRIPTION,
|
||||||
|
"TicketClose" => CLOSE_DESCRIPTION,
|
||||||
|
"TicketRelationRecord" => RELATION_RECORD_DESCRIPTION,
|
||||||
|
"TicketRelationQuery" => RELATION_QUERY_DESCRIPTION,
|
||||||
|
"TicketOrchestrationPlanRecord" => ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
||||||
|
"TicketOrchestrationPlanQuery" => ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
||||||
|
"TicketDoctor" => DOCTOR_DESCRIPTION,
|
||||||
|
_ => "Ticket backend tool.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the model-visible Ticket tool description for a configured Ticket backend.
|
||||||
|
///
|
||||||
|
/// `record_language` is the durable Ticket record/tool-body language, distinct from
|
||||||
|
/// worker response language and Memory/Knowledge language. Keeping this on the tool
|
||||||
|
/// surface ensures every Ticket-capable Pod sees the policy without hidden context
|
||||||
|
/// injection or role-launch-only prose.
|
||||||
|
pub fn ticket_tool_description(name: &str, record_language: Option<&str>) -> String {
|
||||||
|
let mut description = base_tool_description(name).to_string();
|
||||||
|
if let Some(language) = record_language.filter(|language| !language.trim().is_empty()) {
|
||||||
|
description.push_str("\n\nTicket record language: ");
|
||||||
|
description.push_str(language.trim());
|
||||||
|
description.push_str(". Use this language for durable Ticket record and Ticket tool body text, including Ticket item bodies, thread comments/plans/decisions/implementation reports, reviews, resolutions, intake summaries, and orchestration plan notes. This policy is distinct from worker.language for normal prose and memory.language for Memory/Knowledge. Preserve protocol literals, file paths, commands, logs, identifiers, and quoted external text when translation would reduce fidelity.");
|
||||||
|
}
|
||||||
|
description
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||||
struct TicketCreateParams {
|
struct TicketCreateParams {
|
||||||
/// Ticket title. Must not be empty.
|
/// Ticket title. Must not be empty.
|
||||||
|
|
@ -1273,18 +1308,15 @@ fn json_output(summary: String, value: impl Serialize) -> ToolOutput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_definition<T>(
|
fn tool_definition<T>(name: &'static str, backend: LocalTicketBackend) -> ToolDefinition
|
||||||
name: &'static str,
|
|
||||||
description: &'static str,
|
|
||||||
backend: LocalTicketBackend,
|
|
||||||
) -> ToolDefinition
|
|
||||||
where
|
where
|
||||||
T: Tool + From<LocalTicketBackend> + 'static,
|
T: Tool + From<LocalTicketBackend> + 'static,
|
||||||
{
|
{
|
||||||
|
let description = ticket_tool_description(name, backend.record_language());
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema_value = input_schema(name);
|
let schema_value = input_schema(name);
|
||||||
let meta = ToolMeta::new(name)
|
let meta = ToolMeta::new(name)
|
||||||
.description(description)
|
.description(description.clone())
|
||||||
.input_schema(schema_value);
|
.input_schema(schema_value);
|
||||||
let tool: Arc<dyn Tool> = Arc::new(T::from(backend.clone()));
|
let tool: Arc<dyn Tool> = Arc::new(T::from(backend.clone()));
|
||||||
(meta, tool)
|
(meta, tool)
|
||||||
|
|
@ -1348,43 +1380,25 @@ impl_from_backend!(TicketDoctorTool);
|
||||||
/// Build all MVP Ticket tool definitions over one local backend root.
|
/// Build all MVP Ticket tool definitions over one local backend root.
|
||||||
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
tool_definition::<TicketCreateTool>("TicketCreate", CREATE_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()),
|
||||||
tool_definition::<TicketListTool>("TicketList", LIST_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketListTool>("TicketList", backend.clone()),
|
||||||
tool_definition::<TicketShowTool>("TicketShow", SHOW_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketShowTool>("TicketShow", backend.clone()),
|
||||||
tool_definition::<TicketCommentTool>("TicketComment", COMMENT_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketCommentTool>("TicketComment", backend.clone()),
|
||||||
tool_definition::<TicketReviewTool>("TicketReview", REVIEW_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketReviewTool>("TicketReview", backend.clone()),
|
||||||
tool_definition::<TicketIntakeReadyTool>(
|
tool_definition::<TicketIntakeReadyTool>("TicketIntakeReady", backend.clone()),
|
||||||
"TicketIntakeReady",
|
tool_definition::<TicketWorkflowStateTool>("TicketWorkflowState", backend.clone()),
|
||||||
INTAKE_READY_DESCRIPTION,
|
tool_definition::<TicketCloseTool>("TicketClose", backend.clone()),
|
||||||
backend.clone(),
|
tool_definition::<TicketRelationRecordTool>("TicketRelationRecord", backend.clone()),
|
||||||
),
|
tool_definition::<TicketRelationQueryTool>("TicketRelationQuery", backend.clone()),
|
||||||
tool_definition::<TicketWorkflowStateTool>(
|
|
||||||
"TicketWorkflowState",
|
|
||||||
WORKFLOW_STATE_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketCloseTool>("TicketClose", CLOSE_DESCRIPTION, backend.clone()),
|
|
||||||
tool_definition::<TicketRelationRecordTool>(
|
|
||||||
"TicketRelationRecord",
|
|
||||||
RELATION_RECORD_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketRelationQueryTool>(
|
|
||||||
"TicketRelationQuery",
|
|
||||||
RELATION_QUERY_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
||||||
"TicketOrchestrationPlanRecord",
|
"TicketOrchestrationPlanRecord",
|
||||||
ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
|
||||||
backend.clone(),
|
backend.clone(),
|
||||||
),
|
),
|
||||||
tool_definition::<TicketOrchestrationPlanQueryTool>(
|
tool_definition::<TicketOrchestrationPlanQueryTool>(
|
||||||
"TicketOrchestrationPlanQuery",
|
"TicketOrchestrationPlanQuery",
|
||||||
ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
|
||||||
backend.clone(),
|
backend.clone(),
|
||||||
),
|
),
|
||||||
tool_definition::<TicketDoctorTool>("TicketDoctor", DOCTOR_DESCRIPTION, backend),
|
tool_definition::<TicketDoctorTool>("TicketDoctor", backend),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1412,6 +1426,16 @@ mod tests {
|
||||||
.expect("tool exists")
|
.expect("tool exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_description_by_name(backend: LocalTicketBackend, name: &str) -> String {
|
||||||
|
ticket_tools(backend)
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|definition| {
|
||||||
|
let (meta, _) = definition();
|
||||||
|
(meta.name == name).then_some(meta.description)
|
||||||
|
})
|
||||||
|
.expect("tool exists")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_tool_name_partitions_are_explicit() {
|
fn ticket_tool_name_partitions_are_explicit() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1463,6 +1487,29 @@ mod tests {
|
||||||
assert!(meta.description.contains("implementation side effects"));
|
assert!(meta.description.contains("implementation side effects"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_descriptions_include_configured_ticket_record_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let backend = backend(&temp).with_record_language(Some("Japanese"));
|
||||||
|
let description = tool_description_by_name(backend, "TicketComment");
|
||||||
|
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("durable Ticket record and Ticket tool body text"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("memory.language"));
|
||||||
|
assert!(description.contains("Preserve protocol literals"));
|
||||||
|
assert!(description.contains("file paths, commands, logs, identifiers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_descriptions_omit_ticket_record_language_guidance_when_unset() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let description = tool_description_by_name(backend(&temp), "TicketComment");
|
||||||
|
|
||||||
|
assert!(!description.contains("Ticket record language:"));
|
||||||
|
assert!(!description.contains("worker.language"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ticket_tools_create_list_show_and_doctor() {
|
async fn ticket_tools_create_list_show_and_doctor() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -2256,7 +2303,6 @@ mod tests {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
let create = tool(tool_definition::<TicketCreateTool>(
|
let create = tool(tool_definition::<TicketCreateTool>(
|
||||||
"TicketCreate",
|
"TicketCreate",
|
||||||
CREATE_DESCRIPTION,
|
|
||||||
backend(&temp),
|
backend(&temp),
|
||||||
));
|
));
|
||||||
let _ = create;
|
let _ = create;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user