feat: route ticket tools through workspace backend

This commit is contained in:
2026-07-16 01:18:13 +09:00
parent 66abd53e51
commit 983ffe6017
11 changed files with 1002 additions and 110 deletions
+230 -33
View File
@@ -82,7 +82,7 @@ fn io_err(path: impl Into<PathBuf>, source: io::Error) -> TicketError {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum TicketStatus {
Open,
Closed,
@@ -111,7 +111,7 @@ impl fmt::Display for TicketStatus {
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ExtensibleTicketStatus {
Open,
Closed,
@@ -155,7 +155,8 @@ impl From<TicketStatus> for ExtensibleTicketStatus {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TicketWorkflowState {
Planning,
Ready,
@@ -221,7 +222,7 @@ impl fmt::Display for TicketWorkflowState {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkdownText(pub String);
impl MarkdownText {
@@ -246,7 +247,7 @@ impl From<String> for MarkdownText {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TicketIdOrSlug {
Id(String),
Slug(String),
@@ -273,7 +274,8 @@ impl From<String> for TicketIdOrSlug {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TicketEventKind {
Create,
Comment,
@@ -340,7 +342,8 @@ impl From<&str> for TicketEventKind {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TicketReviewResult {
Approve,
RequestChanges,
@@ -375,13 +378,13 @@ impl From<&str> for TicketReviewResult {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketReference {
pub kind: String,
pub target: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewTicketEvent {
pub kind: TicketEventKind,
pub author: Option<String>,
@@ -400,7 +403,7 @@ impl NewTicketEvent {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketStateChange {
pub from: String,
pub to: String,
@@ -428,7 +431,7 @@ impl TicketStateChange {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketIntakeSummary {
pub author: Option<String>,
pub body: MarkdownText,
@@ -445,7 +448,7 @@ impl TicketIntakeSummary {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketReview {
pub result: TicketReviewResult,
pub author: Option<String>,
@@ -470,7 +473,7 @@ impl TicketReview {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewTicket {
pub title: String,
pub slug: Option<String>,
@@ -507,7 +510,7 @@ impl NewTicket {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketFilter {
pub state: Option<TicketWorkflowState>,
}
@@ -522,7 +525,7 @@ impl TicketFilter {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketRef {
pub id: String,
pub slug: String,
@@ -568,7 +571,7 @@ impl fmt::Display for TicketRelationKind {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewTicketRelation {
pub kind: TicketRelationKind,
pub target: String,
@@ -596,7 +599,7 @@ struct TicketRelationArtifact {
relations: Vec<TicketRelation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DerivedTicketRelation {
pub source_ticket: String,
pub inverse_kind: String,
@@ -606,7 +609,7 @@ pub struct DerivedTicketRelation {
pub at: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketRelationBlocker {
pub blocking_ticket: String,
pub reason_kind: String,
@@ -615,14 +618,14 @@ pub struct TicketRelationBlocker {
pub blocking_state: TicketWorkflowState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketRelationNotice {
pub related_ticket: String,
pub kind: TicketRelationKind,
pub message: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketRelationView {
pub outgoing: Vec<TicketRelation>,
pub incoming: Vec<DerivedTicketRelation>,
@@ -702,7 +705,7 @@ pub struct AcceptedOrchestrationPlan {
pub role_plan: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewOrchestrationPlanRecord {
pub kind: OrchestrationPlanKind,
pub related_ticket: Option<String>,
@@ -727,7 +730,7 @@ pub struct OrchestrationPlanRecord {
pub at: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketMeta {
pub id: String,
pub slug: String,
@@ -748,7 +751,7 @@ pub struct TicketMeta {
pub raw: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketSummary {
pub id: String,
pub slug: String,
@@ -765,31 +768,31 @@ pub struct TicketSummary {
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketInvalidRecord {
pub label: String,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketPartialList {
pub tickets: Vec<TicketSummary>,
pub invalid_records: Vec<TicketInvalidRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketPartial {
pub ticket: Ticket,
pub invalid_records: Vec<TicketInvalidRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketDocument {
pub body: MarkdownText,
pub raw_frontmatter: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketEvent {
pub kind: TicketEventKind,
pub author: Option<String>,
@@ -805,13 +808,13 @@ pub struct TicketEvent {
pub attributes: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketArtifactRef {
/// Path relative to the ticket's `artifacts/` directory.
pub relative_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ticket {
pub meta: TicketMeta,
pub document: TicketDocument,
@@ -821,20 +824,20 @@ pub struct Ticket {
pub resolution: Option<MarkdownText>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TicketDoctorSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketDoctorDiagnostic {
pub severity: TicketDoctorSeverity,
pub message: String,
pub path: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TicketDoctorReport {
pub diagnostics: Vec<TicketDoctorDiagnostic>,
}
@@ -869,6 +872,7 @@ impl TicketDoctorReport {
}
pub trait TicketBackend {
fn default_intake_ready_state_change_body(&self, from: &str) -> String;
fn list(&self, filter: TicketFilter) -> Result<Vec<TicketSummary>>;
fn show(&self, id: TicketIdOrSlug) -> Result<Ticket>;
fn create(&self, input: NewTicket) -> Result<TicketRef>;
@@ -915,6 +919,195 @@ pub trait TicketBackend {
fn doctor(&self) -> Result<TicketDoctorReport>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "operation", rename_all = "snake_case")]
pub enum TicketBackendOperation {
DefaultIntakeReadyStateChangeBody {
from: String,
},
List {
filter: TicketFilter,
},
Show {
id: TicketIdOrSlug,
},
Create {
input: NewTicket,
},
AddEvent {
id: TicketIdOrSlug,
event: NewTicketEvent,
},
AddStateChanged {
id: TicketIdOrSlug,
change: TicketStateChange,
},
AddIntakeSummary {
id: TicketIdOrSlug,
summary: TicketIntakeSummary,
},
SetStateField {
id: TicketIdOrSlug,
field: String,
change: TicketStateChange,
},
SetWorkflowState {
id: TicketIdOrSlug,
change: TicketStateChange,
},
MarkIntakeReady {
id: TicketIdOrSlug,
summary: TicketIntakeSummary,
change: TicketStateChange,
},
QueueReady {
id: TicketIdOrSlug,
queued_by: String,
},
Review {
id: TicketIdOrSlug,
review: TicketReview,
},
Close {
id: TicketIdOrSlug,
resolution: MarkdownText,
},
AddTicketRelation {
id: TicketIdOrSlug,
relation: NewTicketRelation,
},
QueryTicketRelations {
ticket: Option<TicketIdOrSlug>,
kind: Option<TicketRelationKind>,
},
RelationView {
id: TicketIdOrSlug,
},
AddOrchestrationPlanRecord {
id: TicketIdOrSlug,
record: NewOrchestrationPlanRecord,
},
QueryOrchestrationPlanRecords {
ticket: Option<TicketIdOrSlug>,
kind: Option<OrchestrationPlanKind>,
},
Doctor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "result", content = "value", rename_all = "snake_case")]
pub enum TicketBackendOperationResult {
Text(String),
Unit,
Tickets(Vec<TicketSummary>),
Ticket(Ticket),
TicketRef(TicketRef),
Relation(TicketRelation),
Relations(Vec<TicketRelation>),
RelationView(TicketRelationView),
OrchestrationPlanRecord(OrchestrationPlanRecord),
OrchestrationPlanRecords(Vec<OrchestrationPlanRecord>),
DoctorReport(TicketDoctorReport),
}
pub fn execute_ticket_backend_operation<B>(
backend: &B,
operation: TicketBackendOperation,
) -> Result<TicketBackendOperationResult>
where
B: TicketBackend + ?Sized,
{
Ok(match operation {
TicketBackendOperation::DefaultIntakeReadyStateChangeBody { from } => {
TicketBackendOperationResult::Text(
backend.default_intake_ready_state_change_body(&from),
)
}
TicketBackendOperation::List { filter } => {
TicketBackendOperationResult::Tickets(backend.list(filter)?)
}
TicketBackendOperation::Show { id } => {
TicketBackendOperationResult::Ticket(backend.show(id)?)
}
TicketBackendOperation::Create { input } => {
TicketBackendOperationResult::TicketRef(backend.create(input)?)
}
TicketBackendOperation::AddEvent { id, event } => {
backend.add_event(id, event)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::AddStateChanged { id, change } => {
backend.add_state_changed(id, change)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::AddIntakeSummary { id, summary } => {
backend.add_intake_summary(id, summary)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::SetStateField { id, field, change } => {
backend.set_state_field(id, &field, change)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::SetWorkflowState { id, change } => {
backend.set_workflow_state(id, change)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::MarkIntakeReady {
id,
summary,
change,
} => {
backend.mark_intake_ready(id, summary, change)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::QueueReady { id, queued_by } => {
backend.queue_ready(id, &queued_by)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::Review { id, review } => {
backend.review(id, review)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::Close { id, resolution } => {
backend.close(id, resolution)?;
TicketBackendOperationResult::Unit
}
TicketBackendOperation::AddTicketRelation { id, relation } => {
TicketBackendOperationResult::Relation(backend.add_ticket_relation(id, relation)?)
}
TicketBackendOperation::QueryTicketRelations { ticket, kind } => {
TicketBackendOperationResult::Relations(backend.query_ticket_relations(ticket, kind)?)
}
TicketBackendOperation::RelationView { id } => {
TicketBackendOperationResult::RelationView(backend.relation_view(id)?)
}
TicketBackendOperation::AddOrchestrationPlanRecord { id, record } => {
TicketBackendOperationResult::OrchestrationPlanRecord(
backend.add_orchestration_plan_record(id, record)?,
)
}
TicketBackendOperation::QueryOrchestrationPlanRecords { ticket, kind } => {
TicketBackendOperationResult::OrchestrationPlanRecords(
backend.query_orchestration_plan_records(ticket, kind)?,
)
}
TicketBackendOperation::Doctor => {
TicketBackendOperationResult::DoctorReport(backend.doctor()?)
}
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum TicketBackendHttpResponse {
Ok {
result: TicketBackendOperationResult,
},
Error {
message: String,
},
}
#[derive(Debug, Clone)]
pub struct LocalTicketBackend {
root: PathBuf,
@@ -1392,6 +1585,10 @@ impl LocalTicketBackend {
}
impl TicketBackend for LocalTicketBackend {
fn default_intake_ready_state_change_body(&self, from: &str) -> String {
self.default_intake_ready_state_change_body(from)
}
fn list(&self, filter: TicketFilter) -> Result<Vec<TicketSummary>> {
let mut tickets = Vec::new();
for dir in self.iter_ticket_dirs(filter)? {
+182 -26
View File
@@ -13,10 +13,11 @@ use serde_json::{Value, json};
use crate::{
AcceptedOrchestrationPlan, LocalTicketBackend, MarkdownText, NewOrchestrationPlanRecord,
NewTicket, NewTicketEvent, NewTicketRelation, OrchestrationPlanKind, Ticket, TicketBackend,
TicketDoctorDiagnostic, TicketDoctorReport, TicketDoctorSeverity, TicketError, TicketEventKind,
TicketIdOrSlug, TicketIntakeSummary, TicketRelationKind, TicketReview, TicketReviewResult,
TicketStateChange, TicketSummary, TicketWorkflowState,
NewTicket, NewTicketEvent, NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord,
Result as TicketResult, Ticket, TicketBackend, TicketDoctorDiagnostic, TicketDoctorReport,
TicketDoctorSeverity, TicketError, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary,
TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, TicketReview,
TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState,
};
const DEFAULT_LIST_LIMIT: usize = 50;
@@ -166,6 +167,160 @@ pub fn ticket_tool_description(name: &str, record_language: Option<&str>) -> Str
description
}
/// Backend object used by the LLM-facing Ticket tools.
///
/// Tool execution is intentionally parameterized by this wrapper rather than by
/// `LocalTicketBackend` so Worker hosts can supply an API-backed implementation
/// without changing the model-visible tool surface.
#[derive(Clone)]
pub struct TicketToolBackend {
backend: Arc<dyn TicketBackend + Send + Sync>,
record_language: Option<String>,
}
impl TicketToolBackend {
pub fn new<B>(backend: B) -> Self
where
B: TicketBackend + Send + Sync + 'static,
{
Self {
backend: Arc::new(backend),
record_language: None,
}
}
pub fn with_record_language(mut self, language: Option<&str>) -> Self {
self.record_language = language
.map(str::trim)
.filter(|language| !language.is_empty())
.map(str::to_string);
self
}
pub fn record_language(&self) -> Option<&str> {
self.record_language.as_deref()
}
}
impl From<LocalTicketBackend> for TicketToolBackend {
fn from(backend: LocalTicketBackend) -> Self {
let record_language = backend.record_language().map(str::to_string);
Self::new(backend).with_record_language(record_language.as_deref())
}
}
impl TicketBackend for TicketToolBackend {
fn default_intake_ready_state_change_body(&self, from: &str) -> String {
self.backend.default_intake_ready_state_change_body(from)
}
fn list(&self, filter: crate::TicketFilter) -> TicketResult<Vec<TicketSummary>> {
self.backend.list(filter)
}
fn show(&self, id: TicketIdOrSlug) -> TicketResult<Ticket> {
self.backend.show(id)
}
fn create(&self, input: NewTicket) -> TicketResult<TicketRef> {
self.backend.create(input)
}
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> TicketResult<()> {
self.backend.add_event(id, event)
}
fn add_state_changed(&self, id: TicketIdOrSlug, change: TicketStateChange) -> TicketResult<()> {
self.backend.add_state_changed(id, change)
}
fn add_intake_summary(
&self,
id: TicketIdOrSlug,
summary: TicketIntakeSummary,
) -> TicketResult<()> {
self.backend.add_intake_summary(id, summary)
}
fn set_state_field(
&self,
id: TicketIdOrSlug,
field: &str,
change: TicketStateChange,
) -> TicketResult<()> {
self.backend.set_state_field(id, field, change)
}
fn set_workflow_state(
&self,
id: TicketIdOrSlug,
change: TicketStateChange,
) -> TicketResult<()> {
self.backend.set_workflow_state(id, change)
}
fn mark_intake_ready(
&self,
id: TicketIdOrSlug,
summary: TicketIntakeSummary,
change: TicketStateChange,
) -> TicketResult<()> {
self.backend.mark_intake_ready(id, summary, change)
}
fn queue_ready(&self, id: TicketIdOrSlug, queued_by: &str) -> TicketResult<()> {
self.backend.queue_ready(id, queued_by)
}
fn review(&self, id: TicketIdOrSlug, review: TicketReview) -> TicketResult<()> {
self.backend.review(id, review)
}
fn close(&self, id: TicketIdOrSlug, resolution: MarkdownText) -> TicketResult<()> {
self.backend.close(id, resolution)
}
fn add_ticket_relation(
&self,
id: TicketIdOrSlug,
relation: NewTicketRelation,
) -> TicketResult<TicketRelation> {
self.backend.add_ticket_relation(id, relation)
}
fn query_ticket_relations(
&self,
ticket: Option<TicketIdOrSlug>,
kind: Option<TicketRelationKind>,
) -> TicketResult<Vec<TicketRelation>> {
self.backend.query_ticket_relations(ticket, kind)
}
fn relation_view(&self, id: TicketIdOrSlug) -> TicketResult<TicketRelationView> {
self.backend.relation_view(id)
}
fn add_orchestration_plan_record(
&self,
id: TicketIdOrSlug,
record: NewOrchestrationPlanRecord,
) -> TicketResult<OrchestrationPlanRecord> {
self.backend.add_orchestration_plan_record(id, record)
}
fn query_orchestration_plan_records(
&self,
ticket: Option<TicketIdOrSlug>,
kind: Option<OrchestrationPlanKind>,
) -> TicketResult<Vec<OrchestrationPlanRecord>> {
self.backend.query_orchestration_plan_records(ticket, kind)
}
fn doctor(&self) -> TicketResult<TicketDoctorReport> {
self.backend.doctor()
}
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TicketCreateParams {
/// Ticket title. Must not be empty.
@@ -565,67 +720,67 @@ struct TicketDoctorOutput {
#[derive(Clone)]
struct TicketCreateTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketListTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketShowTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketCommentTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketReviewTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketIntakeReadyTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketWorkflowStateTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketCloseTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketRelationRecordTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketRelationQueryTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketOrchestrationPlanRecordTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketOrchestrationPlanQueryTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[derive(Clone)]
struct TicketDoctorTool {
backend: LocalTicketBackend,
backend: TicketToolBackend,
}
#[async_trait]
@@ -1308,9 +1463,9 @@ fn json_output(summary: String, value: impl Serialize) -> ToolOutput {
}
}
fn tool_definition<T>(name: &'static str, backend: LocalTicketBackend) -> ToolDefinition
fn tool_definition<T>(name: &'static str, backend: TicketToolBackend) -> ToolDefinition
where
T: Tool + From<LocalTicketBackend> + 'static,
T: Tool + From<TicketToolBackend> + 'static,
{
let description = ticket_tool_description(name, backend.record_language());
Arc::new(move || {
@@ -1355,8 +1510,8 @@ fn input_schema(name: &str) -> Value {
macro_rules! impl_from_backend {
($tool:ident) => {
impl From<LocalTicketBackend> for $tool {
fn from(backend: LocalTicketBackend) -> Self {
impl From<TicketToolBackend> for $tool {
fn from(backend: TicketToolBackend) -> Self {
Self { backend }
}
}
@@ -1377,8 +1532,9 @@ impl_from_backend!(TicketOrchestrationPlanRecordTool);
impl_from_backend!(TicketOrchestrationPlanQueryTool);
impl_from_backend!(TicketDoctorTool);
/// Build all MVP Ticket tool definitions over one local backend root.
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
/// Build all MVP Ticket tool definitions over the supplied backend.
pub fn ticket_tools(backend: impl Into<TicketToolBackend>) -> Vec<ToolDefinition> {
let backend = backend.into();
vec![
tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()),
tool_definition::<TicketListTool>("TicketList", backend.clone()),
@@ -1416,7 +1572,7 @@ mod tests {
tool
}
fn tool_by_name(backend: LocalTicketBackend, name: &str) -> Arc<dyn Tool> {
fn tool_by_name(backend: impl Into<TicketToolBackend>, name: &str) -> Arc<dyn Tool> {
ticket_tools(backend)
.into_iter()
.find_map(|definition| {
@@ -1426,7 +1582,7 @@ mod tests {
.expect("tool exists")
}
fn tool_description_by_name(backend: LocalTicketBackend, name: &str) -> String {
fn tool_description_by_name(backend: impl Into<TicketToolBackend>, name: &str) -> String {
ticket_tools(backend)
.into_iter()
.find_map(|definition| {
@@ -2303,7 +2459,7 @@ mod tests {
let temp = TempDir::new().unwrap();
let create = tool(tool_definition::<TicketCreateTool>(
"TicketCreate",
backend(&temp),
backend(&temp).into(),
));
let _ = create;
}