feat: route ticket tools through workspace backend
This commit is contained in:
+230
-33
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user