merge: integrate orchestration branch
This commit is contained in:
@@ -125,6 +125,10 @@ impl PendingRun {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_auto_run_notification(status: PodStatus, auto_run: bool) -> bool {
|
||||
auto_run && status == PodStatus::Idle
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PodController — actor that owns a Pod
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -774,7 +778,7 @@ async fn controller_loop<C, St>(
|
||||
pending = Some(PendingRun::Run(input));
|
||||
}
|
||||
|
||||
Method::Notify { message } => {
|
||||
Method::Notify { message, auto_run } => {
|
||||
// Client-side live echo is delivered as `Event::SystemItem`
|
||||
// once the interceptor commits the corresponding
|
||||
// `LogEntry::SystemItem` entry — drained out of the
|
||||
@@ -784,10 +788,10 @@ async fn controller_loop<C, St>(
|
||||
// RUNNING / Paused: the buffer push is the entire
|
||||
// operation; an in-flight turn (or the next
|
||||
// Resume/Run) will drain it at its next
|
||||
// pending_history_appends. IDLE: auto-start a turn so the LLM
|
||||
// sees the buffered notification(s) without a human
|
||||
// Run.
|
||||
if shared_state.get_status() == PodStatus::Idle {
|
||||
// pending_history_appends. IDLE: only `auto_run`
|
||||
// notifications stage RunForNotification; weak progress
|
||||
// notices stay queued until an explicit run/resume.
|
||||
if should_auto_run_notification(shared_state.get_status(), auto_run) {
|
||||
pending = Some(PendingRun::RunForNotification(protocol::InvokeKind::Notify));
|
||||
}
|
||||
}
|
||||
@@ -1145,7 +1149,7 @@ where
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
Some(Method::Notify { message }) => {
|
||||
Some(Method::Notify { message, .. }) => {
|
||||
// Live echo arrives via `Event::SystemItem` once
|
||||
// the in-flight turn's next `pending_history_appends`
|
||||
// drains this entry through the interceptor.
|
||||
@@ -1337,6 +1341,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_auto_run_gate_only_allows_idle_auto_run() {
|
||||
assert!(should_auto_run_notification(PodStatus::Idle, true));
|
||||
assert!(!should_auto_run_notification(PodStatus::Idle, false));
|
||||
assert!(!should_auto_run_notification(PodStatus::Running, true));
|
||||
assert!(!should_auto_run_notification(PodStatus::Paused, true));
|
||||
}
|
||||
|
||||
struct DriveTurnEnv {
|
||||
// Held to keep the channel alive; without this `method_rx.recv()`
|
||||
// would observe channel-closed and confuse the select! arm.
|
||||
|
||||
@@ -913,7 +913,14 @@ where
|
||||
}
|
||||
|
||||
async fn send_peer_notify(socket_path: &Path, message: String) -> io::Result<()> {
|
||||
connect_and_send(socket_path, &Method::Notify { message }).await
|
||||
connect_and_send(
|
||||
socket_path,
|
||||
&Method::Notify {
|
||||
message,
|
||||
auto_run: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn json_content<T: Serialize>(value: &T) -> Result<String, ToolError> {
|
||||
@@ -1395,7 +1402,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let method = reader.next::<Method>().await.unwrap().unwrap();
|
||||
if let Method::Notify { message } = method {
|
||||
if let Method::Notify { message, auto_run } = method {
|
||||
assert!(auto_run);
|
||||
tx.send(message).await.unwrap();
|
||||
} else {
|
||||
panic!("expected Notify, got {method:?}");
|
||||
|
||||
@@ -12,7 +12,7 @@ use ticket::{
|
||||
tool::{
|
||||
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_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();
|
||||
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
|
||||
}
|
||||
@@ -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 {
|
||||
TicketFeature::for_workspace(workspace)
|
||||
}
|
||||
@@ -298,6 +270,19 @@ mod tests {
|
||||
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]
|
||||
fn descriptor_declares_ticket_tools_and_backend_authority() {
|
||||
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]
|
||||
fn lifecycle_installation_exposes_lifecycle_tools() {
|
||||
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]
|
||||
fn installs_ticket_tools_when_default_root_is_usable() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
@@ -1025,6 +1025,7 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "turn finished".into(),
|
||||
auto_run: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1105,6 +1106,62 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_while_idle_with_auto_run_false_waits_for_explicit_run() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
let client_for_assert = client.clone();
|
||||
let pod = make_pod(client).await;
|
||||
let handle = spawn_controller(pod).await;
|
||||
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "progress snapshot".into(),
|
||||
auto_run: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
assert_eq!(handle.shared_state.get_status(), PodStatus::Idle);
|
||||
assert!(
|
||||
client_for_assert.captured_requests().is_empty(),
|
||||
"weak Notify must not stage RunForNotification while idle"
|
||||
);
|
||||
|
||||
handle.send(Method::run_text("continue")).await.unwrap();
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
loop {
|
||||
if !client_for_assert.captured_requests().is_empty() {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
"explicit run did not reach the mock LLM"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
wait_for_status(&handle, PodStatus::Idle).await;
|
||||
let requests = client_for_assert.captured_requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
1,
|
||||
"explicit run should drain the queued notification"
|
||||
);
|
||||
let notify_in_request = requests[0].items.iter().any(|i| {
|
||||
i.as_text()
|
||||
.is_some_and(|t| t.contains("[Notification]") && t.contains("progress snapshot"))
|
||||
});
|
||||
assert!(
|
||||
notify_in_request,
|
||||
"queued weak notification must be history-backed on the next explicit run; got items: {:?}",
|
||||
requests[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| i.as_text())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||
let client = MockClient::new(simple_text_events());
|
||||
@@ -1259,6 +1316,7 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||
handle
|
||||
.send(Method::Notify {
|
||||
message: "ping".into(),
|
||||
auto_run: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -4,6 +4,14 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_true(value: &bool) -> bool {
|
||||
*value
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Method (Client → Pod via Unix Socket)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -15,10 +23,15 @@ pub enum Method {
|
||||
input: Vec<Segment>,
|
||||
},
|
||||
/// Human-readable text injected into the target Pod's LLM context
|
||||
/// as a non-blocking system message. No side effects beyond LLM
|
||||
/// context; use `PodEvent` for typed lifecycle reports.
|
||||
/// as a non-blocking system message. `auto_run` controls whether an
|
||||
/// idle target is kicked into `RunForNotification`; weak notifications
|
||||
/// (`auto_run: false`) are only queued for the next turn/resume/run.
|
||||
/// No side effects beyond LLM context; use `PodEvent` for typed
|
||||
/// lifecycle reports.
|
||||
Notify {
|
||||
message: String,
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
auto_run: bool,
|
||||
},
|
||||
/// Typed lifecycle report from a child Pod to its direct parent.
|
||||
PodEvent(PodEvent),
|
||||
@@ -1027,17 +1040,28 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_notify_json_roundtrip() {
|
||||
fn method_notify_json_roundtrip_defaults_to_auto_run() {
|
||||
let json = r#"{"method":"notify","params":{"message":"turn done"}}"#;
|
||||
let method: Method = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(
|
||||
method,
|
||||
Method::Notify { ref message } if message == "turn done"
|
||||
Method::Notify { ref message, auto_run: true } if message == "turn done"
|
||||
));
|
||||
let serialized = serde_json::to_string(&method).unwrap();
|
||||
assert_eq!(serialized, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_notify_weak_json_roundtrip_serializes_auto_run_false() {
|
||||
let json = r#"{"method":"notify","params":{"message":"progress","auto_run":false}}"#;
|
||||
let method: Method = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(
|
||||
method,
|
||||
Method::Notify { ref message, auto_run: false } if message == "progress"
|
||||
));
|
||||
assert_eq!(serde_json::to_string(&method).unwrap(), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_list_completions_roundtrip() {
|
||||
let method = Method::ListCompletions {
|
||||
|
||||
+82
-36
@@ -131,6 +131,41 @@ explicit state decisions.";
|
||||
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
||||
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)]
|
||||
struct TicketCreateParams {
|
||||
/// Ticket title. Must not be empty.
|
||||
@@ -1273,18 +1308,15 @@ fn json_output(summary: String, value: impl Serialize) -> ToolOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_definition<T>(
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
backend: LocalTicketBackend,
|
||||
) -> ToolDefinition
|
||||
fn tool_definition<T>(name: &'static str, backend: LocalTicketBackend) -> ToolDefinition
|
||||
where
|
||||
T: Tool + From<LocalTicketBackend> + 'static,
|
||||
{
|
||||
let description = ticket_tool_description(name, backend.record_language());
|
||||
Arc::new(move || {
|
||||
let schema_value = input_schema(name);
|
||||
let meta = ToolMeta::new(name)
|
||||
.description(description)
|
||||
.description(description.clone())
|
||||
.input_schema(schema_value);
|
||||
let tool: Arc<dyn Tool> = Arc::new(T::from(backend.clone()));
|
||||
(meta, tool)
|
||||
@@ -1348,43 +1380,25 @@ impl_from_backend!(TicketDoctorTool);
|
||||
/// Build all MVP Ticket tool definitions over one local backend root.
|
||||
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
tool_definition::<TicketCreateTool>("TicketCreate", CREATE_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketListTool>("TicketList", LIST_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketShowTool>("TicketShow", SHOW_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketCommentTool>("TicketComment", COMMENT_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketReviewTool>("TicketReview", REVIEW_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketIntakeReadyTool>(
|
||||
"TicketIntakeReady",
|
||||
INTAKE_READY_DESCRIPTION,
|
||||
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::<TicketCreateTool>("TicketCreate", backend.clone()),
|
||||
tool_definition::<TicketListTool>("TicketList", backend.clone()),
|
||||
tool_definition::<TicketShowTool>("TicketShow", backend.clone()),
|
||||
tool_definition::<TicketCommentTool>("TicketComment", backend.clone()),
|
||||
tool_definition::<TicketReviewTool>("TicketReview", backend.clone()),
|
||||
tool_definition::<TicketIntakeReadyTool>("TicketIntakeReady", backend.clone()),
|
||||
tool_definition::<TicketWorkflowStateTool>("TicketWorkflowState", backend.clone()),
|
||||
tool_definition::<TicketCloseTool>("TicketClose", backend.clone()),
|
||||
tool_definition::<TicketRelationRecordTool>("TicketRelationRecord", backend.clone()),
|
||||
tool_definition::<TicketRelationQueryTool>("TicketRelationQuery", backend.clone()),
|
||||
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
||||
"TicketOrchestrationPlanRecord",
|
||||
ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
||||
backend.clone(),
|
||||
),
|
||||
tool_definition::<TicketOrchestrationPlanQueryTool>(
|
||||
"TicketOrchestrationPlanQuery",
|
||||
ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
||||
backend.clone(),
|
||||
),
|
||||
tool_definition::<TicketDoctorTool>("TicketDoctor", DOCTOR_DESCRIPTION, backend),
|
||||
tool_definition::<TicketDoctorTool>("TicketDoctor", backend),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1412,6 +1426,16 @@ mod tests {
|
||||
.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]
|
||||
fn ticket_tool_name_partitions_are_explicit() {
|
||||
assert_eq!(
|
||||
@@ -1463,6 +1487,29 @@ mod tests {
|
||||
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]
|
||||
async fn ticket_tools_create_list_show_and_doctor() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
@@ -2256,7 +2303,6 @@ mod tests {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let create = tool(tool_definition::<TicketCreateTool>(
|
||||
"TicketCreate",
|
||||
CREATE_DESCRIPTION,
|
||||
backend(&temp),
|
||||
));
|
||||
let _ = create;
|
||||
|
||||
@@ -22,6 +22,7 @@ pod-registry = { workspace = true }
|
||||
provider = { workspace = true }
|
||||
ticket = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
minijinja = "2.19.0"
|
||||
pulldown-cmark = { version = "0.13.3", default-features = false }
|
||||
llm-worker.workspace = true
|
||||
|
||||
|
||||
+1273
-117
File diff suppressed because it is too large
Load Diff
@@ -894,7 +894,7 @@ fn pod_row(entry: &PodListEntry) -> PanelRow {
|
||||
ticket: None,
|
||||
related_pods: Vec::new(),
|
||||
disabled_reason: entry.actions.disabled_reason.clone(),
|
||||
key_hint: Some("Enter opens/attaches; Right marks action focus".to_string()),
|
||||
key_hint: Some("Enter opens/attaches for inspection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user