1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use starweaver_core::{Metadata, RunId, SessionId, TraceContext};
7use starweaver_model::TOOL_RETURN_APPROVAL_ARGUMENTS_METADATA_KEY;
8
9use crate::records::ExecutionStatus;
10
11const fn initial_interaction_revision() -> u64 {
12 1
13}
14
15pub struct ToolReturnRecordInput<'a> {
17 pub session_id: &'a SessionId,
19 pub run_id: &'a RunId,
21 pub tool_call_id: &'a str,
23 pub tool_name: &'a str,
25 pub metadata: &'a Metadata,
27 pub trace_context: Option<&'a TraceContext>,
29 pub policy: Option<Value>,
31}
32
33impl<'a> ToolReturnRecordInput<'a> {
34 #[must_use]
36 pub const fn new(
37 session_id: &'a SessionId,
38 run_id: &'a RunId,
39 tool_call_id: &'a str,
40 tool_name: &'a str,
41 metadata: &'a Metadata,
42 ) -> Self {
43 Self {
44 session_id,
45 run_id,
46 tool_call_id,
47 tool_name,
48 metadata,
49 trace_context: None,
50 policy: None,
51 }
52 }
53
54 #[must_use]
56 pub const fn with_trace_context(mut self, trace_context: &'a TraceContext) -> Self {
57 self.trace_context = Some(trace_context);
58 self
59 }
60
61 #[must_use]
63 pub fn with_policy(mut self, policy: Value) -> Self {
64 self.policy = Some(policy);
65 self
66 }
67
68 fn control_flow(&self) -> Option<&str> {
69 self.metadata.get("control_flow").and_then(Value::as_str)
70 }
71
72 fn approval_id(&self) -> String {
73 format!("approval_{}_{}", self.run_id.as_str(), self.tool_call_id)
74 }
75
76 fn deferred_id(&self) -> String {
77 format!("deferred_{}_{}", self.run_id.as_str(), self.tool_call_id)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum ApprovalStatus {
85 #[default]
87 Pending,
88 Approved,
90 Denied,
92 Expired,
94 Cancelled,
96}
97
98#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100pub struct ApprovalDecision {
101 pub status: ApprovalStatus,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub decided_by: Option<String>,
106 pub decided_at: DateTime<Utc>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub reason: Option<String>,
111 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
113 pub metadata: Metadata,
114}
115
116#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
118pub struct ApprovalRecord {
119 pub approval_id: String,
121 #[serde(default = "initial_interaction_revision")]
123 pub revision: u64,
124 pub session_id: SessionId,
126 pub run_id: RunId,
128 pub action_id: String,
130 pub action_name: String,
132 #[serde(default, skip_serializing_if = "Value::is_null")]
134 pub request: Value,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub reviewed_arguments: Option<Value>,
138 #[serde(default)]
140 pub status: ApprovalStatus,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub decision: Option<ApprovalDecision>,
144 pub created_at: DateTime<Utc>,
146 pub updated_at: DateTime<Utc>,
148 #[serde(default, skip_serializing_if = "TraceContext::is_empty")]
150 pub trace_context: TraceContext,
151 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
153 pub metadata: Metadata,
154}
155
156impl starweaver_core::VersionedRecord for ApprovalRecord {
157 const SCHEMA: &'static str = "starweaver.session.approval_record";
158 const ALLOW_BARE_V0: bool = true;
159}
160
161impl ApprovalRecord {
162 #[must_use]
164 pub fn new(
165 approval_id: impl Into<String>,
166 session_id: SessionId,
167 run_id: RunId,
168 action_id: impl Into<String>,
169 action_name: impl Into<String>,
170 ) -> Self {
171 let now = Utc::now();
172 Self {
173 approval_id: approval_id.into(),
174 revision: initial_interaction_revision(),
175 session_id,
176 run_id,
177 action_id: action_id.into(),
178 action_name: action_name.into(),
179 request: Value::Null,
180 reviewed_arguments: None,
181 status: ApprovalStatus::Pending,
182 decision: None,
183 created_at: now,
184 updated_at: now,
185 trace_context: TraceContext::default(),
186 metadata: Metadata::default(),
187 }
188 }
189
190 #[must_use]
192 pub fn from_tool_return(input: &ToolReturnRecordInput<'_>) -> Option<Self> {
193 if input.control_flow() != Some("approval_required") {
194 return None;
195 }
196 let mut record = Self::new(
197 input.approval_id(),
198 input.session_id.clone(),
199 input.run_id.clone(),
200 input.tool_call_id,
201 input.tool_name,
202 );
203 record.request = input
204 .metadata
205 .get("approval")
206 .cloned()
207 .unwrap_or(Value::Null);
208 record.reviewed_arguments = input
209 .metadata
210 .get(TOOL_RETURN_APPROVAL_ARGUMENTS_METADATA_KEY)
211 .cloned();
212 if let Some(trace_context) = input.trace_context {
213 record.trace_context = trace_context.clone();
214 }
215 if let Some(policy) = &input.policy {
216 record.metadata.insert("policy".to_string(), policy.clone());
217 }
218 Some(record)
219 }
220}
221
222#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224#[serde(tag = "decision", rename_all = "snake_case")]
225pub enum ToolApprovalDecision {
226 Approved {
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 decided_by: Option<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 reason: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 override_arguments: Option<Value>,
237 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
239 metadata: Metadata,
240 },
241 Denied {
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 decided_by: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 reason: Option<String>,
249 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
251 metadata: Metadata,
252 },
253}
254
255impl ToolApprovalDecision {
256 #[must_use]
258 pub fn approved() -> Self {
259 Self::Approved {
260 decided_by: None,
261 reason: None,
262 override_arguments: None,
263 metadata: Metadata::default(),
264 }
265 }
266
267 #[must_use]
269 pub fn denied(reason: impl Into<String>) -> Self {
270 Self::Denied {
271 decided_by: None,
272 reason: Some(reason.into()),
273 metadata: Metadata::default(),
274 }
275 }
276
277 #[must_use]
279 pub fn with_override_arguments(self, arguments: Value) -> Self {
280 match self {
281 Self::Approved {
282 decided_by,
283 reason,
284 metadata,
285 ..
286 } => Self::Approved {
287 decided_by,
288 reason,
289 override_arguments: Some(arguments),
290 metadata,
291 },
292 denied @ Self::Denied { .. } => denied,
293 }
294 }
295
296 #[must_use]
298 pub fn into_approval_decision(self) -> ApprovalDecision {
299 let decided_at = Utc::now();
300 match self {
301 Self::Approved {
302 decided_by,
303 reason,
304 override_arguments,
305 mut metadata,
306 } => {
307 if let Some(arguments) = override_arguments {
308 metadata.insert("override_arguments".to_string(), arguments);
309 }
310 ApprovalDecision {
311 status: ApprovalStatus::Approved,
312 decided_by,
313 decided_at,
314 reason,
315 metadata,
316 }
317 }
318 Self::Denied {
319 decided_by,
320 reason,
321 metadata,
322 } => ApprovalDecision {
323 status: ApprovalStatus::Denied,
324 decided_by,
325 decided_at,
326 reason,
327 metadata,
328 },
329 }
330 }
331}
332
333#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
335pub struct DeferredToolRecord {
336 pub deferred_id: String,
338 #[serde(default = "initial_interaction_revision")]
340 pub revision: u64,
341 pub session_id: SessionId,
343 pub run_id: RunId,
345 pub tool_call_id: String,
347 pub tool_name: String,
349 #[serde(default, skip_serializing_if = "Value::is_null")]
351 pub request: Value,
352 pub status: ExecutionStatus,
354 #[serde(default, skip_serializing_if = "Value::is_null")]
356 pub response: Value,
357 pub created_at: DateTime<Utc>,
359 pub updated_at: DateTime<Utc>,
361 #[serde(default, skip_serializing_if = "TraceContext::is_empty")]
363 pub trace_context: TraceContext,
364 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
366 pub metadata: Metadata,
367}
368
369impl starweaver_core::VersionedRecord for DeferredToolRecord {
370 const SCHEMA: &'static str = "starweaver.session.deferred_tool_record";
371 const ALLOW_BARE_V0: bool = true;
372}
373
374impl DeferredToolRecord {
375 #[must_use]
377 pub fn new(
378 deferred_id: impl Into<String>,
379 session_id: SessionId,
380 run_id: RunId,
381 tool_call_id: impl Into<String>,
382 tool_name: impl Into<String>,
383 ) -> Self {
384 let now = Utc::now();
385 Self {
386 deferred_id: deferred_id.into(),
387 revision: initial_interaction_revision(),
388 session_id,
389 run_id,
390 tool_call_id: tool_call_id.into(),
391 tool_name: tool_name.into(),
392 request: Value::Null,
393 status: ExecutionStatus::Pending,
394 response: Value::Null,
395 created_at: now,
396 updated_at: now,
397 trace_context: TraceContext::default(),
398 metadata: Metadata::default(),
399 }
400 }
401
402 #[must_use]
404 pub fn from_tool_return(input: &ToolReturnRecordInput<'_>) -> Option<Self> {
405 if input.control_flow() != Some("call_deferred") {
406 return None;
407 }
408 let mut record = Self::new(
409 input.deferred_id(),
410 input.session_id.clone(),
411 input.run_id.clone(),
412 input.tool_call_id,
413 input.tool_name,
414 );
415 record.request = input
416 .metadata
417 .get("deferred")
418 .cloned()
419 .unwrap_or(Value::Null);
420 record.status = ExecutionStatus::Waiting;
421 if let Some(trace_context) = input.trace_context {
422 record.trace_context = trace_context.clone();
423 }
424 if let Some(policy) = &input.policy {
425 record.metadata.insert("policy".to_string(), policy.clone());
426 }
427 Some(record)
428 }
429}
430
431#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
433pub struct DeferredToolRequest {
434 pub deferred_id: String,
436 pub session_id: SessionId,
438 pub run_id: RunId,
440 pub tool_call_id: String,
442 pub tool_name: String,
444 #[serde(default, skip_serializing_if = "Value::is_null")]
446 pub arguments: Value,
447 #[serde(default, skip_serializing_if = "TraceContext::is_empty")]
449 pub trace_context: TraceContext,
450 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
452 pub metadata: Metadata,
453}
454
455impl DeferredToolRequest {
456 #[must_use]
458 pub fn from_record(record: &DeferredToolRecord) -> Self {
459 Self {
460 deferred_id: record.deferred_id.clone(),
461 session_id: record.session_id.clone(),
462 run_id: record.run_id.clone(),
463 tool_call_id: record.tool_call_id.clone(),
464 tool_name: record.tool_name.clone(),
465 arguments: record.request.clone(),
466 trace_context: record.trace_context.clone(),
467 metadata: record.metadata.clone(),
468 }
469 }
470
471 #[must_use]
473 pub fn into_record(self) -> DeferredToolRecord {
474 let mut record = DeferredToolRecord::new(
475 self.deferred_id,
476 self.session_id,
477 self.run_id,
478 self.tool_call_id,
479 self.tool_name,
480 );
481 record.request = self.arguments;
482 record.trace_context = self.trace_context;
483 record.metadata = self.metadata;
484 record
485 }
486}
487
488#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
490pub struct DeferredToolRequests {
491 #[serde(default, skip_serializing_if = "Vec::is_empty")]
493 pub requests: Vec<DeferredToolRequest>,
494}
495
496impl DeferredToolRequests {
497 #[must_use]
499 pub fn from_records(records: &[DeferredToolRecord]) -> Self {
500 Self {
501 requests: records
502 .iter()
503 .map(DeferredToolRequest::from_record)
504 .collect(),
505 }
506 }
507
508 #[must_use]
510 pub const fn is_empty(&self) -> bool {
511 self.requests.is_empty()
512 }
513}
514
515#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
517pub struct DeferredToolResult {
518 pub deferred_id: String,
520 pub status: ExecutionStatus,
522 #[serde(default, skip_serializing_if = "Value::is_null")]
524 pub response: Value,
525 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
527 pub metadata: Metadata,
528}
529
530impl DeferredToolResult {
531 #[must_use]
533 pub fn completed(deferred_id: impl Into<String>, response: Value) -> Self {
534 Self {
535 deferred_id: deferred_id.into(),
536 status: ExecutionStatus::Completed,
537 response,
538 metadata: Metadata::default(),
539 }
540 }
541
542 #[must_use]
544 pub fn failed(deferred_id: impl Into<String>, response: Value) -> Self {
545 Self {
546 deferred_id: deferred_id.into(),
547 status: ExecutionStatus::Failed,
548 response,
549 metadata: Metadata::default(),
550 }
551 }
552
553 #[must_use]
555 pub fn cancelled(deferred_id: impl Into<String>, response: Value) -> Self {
556 Self {
557 deferred_id: deferred_id.into(),
558 status: ExecutionStatus::Cancelled,
559 response,
560 metadata: Metadata::default(),
561 }
562 }
563
564 #[must_use]
566 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
567 self.metadata = metadata;
568 self
569 }
570
571 pub fn apply_to_record(self, record: &mut DeferredToolRecord) {
573 record.status = self.status;
574 record.response = self.response;
575 record.updated_at = Utc::now();
576 record.metadata.extend(self.metadata);
577 }
578}
579
580#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
582pub struct DeferredToolResults {
583 #[serde(default, skip_serializing_if = "Vec::is_empty")]
585 pub results: Vec<DeferredToolResult>,
586}
587
588impl DeferredToolResults {
589 #[must_use]
591 pub fn new(results: impl IntoIterator<Item = DeferredToolResult>) -> Self {
592 Self {
593 results: results.into_iter().collect(),
594 }
595 }
596
597 #[must_use]
599 pub const fn is_empty(&self) -> bool {
600 self.results.is_empty()
601 }
602}