1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use stack_ids::{
5 ApplicabilityContextId, ApprovalGrantId, ApprovalRecordId, ArtifactId, AttemptId,
6 AttestationEnvelopeId, CompiledObligationSetId, CompositionReceiptId, ContentDigest,
7 CrossRuntimeReplayTicketId, EffectCommitDecisionId, EffectExecutionReceiptId,
8 EffectiveConstitutionId, ExecutionPermitId, PolicyDecisionId, ProfileSetId,
9 RemoteOracleLeaseId, RemoteSliceResultId, ScopeKey, ToolEffectDispatchReceiptId, TraceCtx,
10 TrialId,
11};
12use std::fmt;
13use thiserror::Error;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ToolExecutionPermitScope {
17 namespace: String,
18 target_key: String,
19}
20
21impl ToolExecutionPermitScope {
22 pub fn namespace(&self) -> &str {
24 &self.namespace
25 }
26
27 pub fn target_key(&self) -> &str {
29 &self.target_key
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ToolExecutionPermit {
35 execution_permit_id: ExecutionPermitId,
36 decision_id: PolicyDecisionId,
37 approval_record_id: Option<ApprovalRecordId>,
38 scope: ToolExecutionPermitScope,
39}
40
41impl ToolExecutionPermit {
42 pub fn new(
44 execution_permit_id: ExecutionPermitId,
45 decision_id: PolicyDecisionId,
46 approval_record_id: Option<ApprovalRecordId>,
47 namespace: impl Into<String>,
48 target_key: impl Into<String>,
49 ) -> Self {
50 Self {
51 execution_permit_id,
52 decision_id,
53 approval_record_id,
54 scope: ToolExecutionPermitScope {
55 namespace: namespace.into(),
56 target_key: target_key.into(),
57 },
58 }
59 }
60
61 pub fn execution_permit_id(&self) -> &ExecutionPermitId {
63 &self.execution_permit_id
64 }
65
66 pub fn decision_id(&self) -> &PolicyDecisionId {
68 &self.decision_id
69 }
70
71 pub fn approval_record_id(&self) -> Option<&ApprovalRecordId> {
73 self.approval_record_id.as_ref()
74 }
75
76 pub fn scope(&self) -> &ToolExecutionPermitScope {
78 &self.scope
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum ToolBackendKind {
85 LocalFunction,
86 OpenAiFunction,
87 OpenAiBuiltIn,
88 OllamaFunction,
89 RemoteMcp,
90}
91
92impl ToolBackendKind {
93 pub fn as_str(&self) -> &'static str {
95 match self {
96 Self::LocalFunction => "local_function",
97 Self::OpenAiFunction => "open_ai_function",
98 Self::OpenAiBuiltIn => "open_ai_built_in",
99 Self::OllamaFunction => "ollama_function",
100 Self::RemoteMcp => "remote_mcp",
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum ToolOutputMode {
108 StructuredJson,
109 Text,
110 ArtifactRefs,
111 JobHandle,
112}
113
114#[derive(
115 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
116)]
117#[serde(rename_all = "snake_case")]
118pub enum ToolSideEffectClass {
119 ReadOnly,
120 Analysis,
121 PreviewWrite,
122 Write,
123 Admin,
124}
125
126impl fmt::Display for ToolSideEffectClass {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.write_str(match self {
129 Self::ReadOnly => "read-only",
130 Self::Analysis => "analysis",
131 Self::PreviewWrite => "preview-write",
132 Self::Write => "write",
133 Self::Admin => "admin",
134 })
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "snake_case")]
140pub enum ToolIdempotencyClass {
141 Idempotent,
142 BestEffort,
143 NonIdempotent,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum ToolApprovalKind {
149 None,
150 UserRequired,
151 PolicyRequired,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum ToolExposureMode {
157 Auto,
158 OptIn,
159 Hidden,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum McpSurfaceKind {
165 None,
166 Tool,
167 Resource,
168 Prompt,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
172#[serde(rename_all = "snake_case")]
173pub enum ToolReceiptPersistence {
174 #[default]
175 Ephemeral,
176 ForgeRaw,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum ToolOriginKind {
182 OpenAiResponses,
183 OpenAiChat,
184 OllamaChat,
185 Local,
186 Mcp,
187 Test,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum ToolPlannerStage {
193 InitialRouting,
194 Retrieval,
195 Analysis,
196 Execution,
197 Audit,
198}
199
200impl ToolPlannerStage {
201 pub fn as_str(&self) -> &'static str {
203 match self {
204 Self::InitialRouting => "initial_routing",
205 Self::Retrieval => "retrieval",
206 Self::Analysis => "analysis",
207 Self::Execution => "execution",
208 Self::Audit => "audit",
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "snake_case")]
215pub enum ToolApprovalState {
216 NotRequired,
217 Approved,
218 Denied,
219}
220
221impl ToolApprovalState {
222 pub fn as_str(&self) -> &'static str {
224 match self {
225 Self::NotRequired => "not_required",
226 Self::Approved => "approved",
227 Self::Denied => "denied",
228 }
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "snake_case")]
234pub enum ToolRetryOwner {
235 LlmPipeline,
236 AgentGraph,
237 JobQueue,
238 AiBatchQueue,
239 ForgeOrchestration,
240 External,
241}
242
243impl ToolRetryOwner {
244 pub fn as_str(&self) -> &'static str {
246 match self {
247 Self::LlmPipeline => "llm_pipeline",
248 Self::AgentGraph => "agent_graph",
249 Self::JobQueue => "job_queue",
250 Self::AiBatchQueue => "ai_batch_queue",
251 Self::ForgeOrchestration => "forge_orchestration",
252 Self::External => "external",
253 }
254 }
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "snake_case")]
259pub enum ApprovalGrantEffectClass {
260 Analysis,
261 PreviewWrite,
262 Write,
263 Admin,
264}
265
266impl ApprovalGrantEffectClass {
267 pub fn for_tool_side_effect_class(side_effect_class: &ToolSideEffectClass) -> Option<Self> {
269 match side_effect_class {
270 ToolSideEffectClass::ReadOnly => None,
271 ToolSideEffectClass::Analysis => Some(Self::Analysis),
272 ToolSideEffectClass::PreviewWrite => Some(Self::PreviewWrite),
273 ToolSideEffectClass::Write => Some(Self::Write),
274 ToolSideEffectClass::Admin => Some(Self::Admin),
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct ApprovalGrantCitation {
281 pub applicability_context_id: ApplicabilityContextId,
282 pub profile_set_id: ProfileSetId,
283 pub composition_receipt_id: CompositionReceiptId,
284 pub effective_constitution_id: EffectiveConstitutionId,
285 pub compiled_obligation_set_id: CompiledObligationSetId,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct ApprovalGrantScope {
290 pub namespace: String,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub target_key: Option<String>,
293 pub tool_name: String,
294 pub effect_class: ApprovalGrantEffectClass,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub planner_stage: Option<ToolPlannerStage>,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct ApprovalGrant {
301 pub grant_id: ApprovalGrantId,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub approval_record_id: Option<ApprovalRecordId>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub decision_id: Option<PolicyDecisionId>,
306 pub policy_version: String,
307 pub scope: ApprovalGrantScope,
308 #[serde(default)]
309 pub approver_lineage: Vec<String>,
310 pub approved_at: String,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub expires_at: Option<String>,
313 pub citation: ApprovalGrantCitation,
314}
315
316impl ApprovalGrant {
317 pub fn new(
319 policy_version: impl Into<String>,
320 scope: ApprovalGrantScope,
321 approver_lineage: Vec<String>,
322 approved_at: impl Into<String>,
323 expires_at: Option<String>,
324 citation: ApprovalGrantCitation,
325 ) -> Self {
326 Self {
327 grant_id: ApprovalGrantId::generate(),
328 approval_record_id: None,
329 decision_id: None,
330 policy_version: policy_version.into(),
331 scope,
332 approver_lineage,
333 approved_at: approved_at.into(),
334 expires_at,
335 citation,
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "snake_case")]
342pub enum ToolErrorClass {
343 InvalidArguments,
344 UnknownTool,
345 ApprovalRequired,
346 Denied,
347 Timeout,
348 Cancelled,
349 OutputTooLarge,
350 Execution,
351 ReceiptPersistence,
352 ProviderContract,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct ToolExposurePolicy {
357 pub exposure_mode: ToolExposureMode,
358 #[serde(default)]
359 pub allow_parallel_calls: bool,
360 #[serde(default)]
361 pub max_calls_per_turn: u32,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
363 pub allowed_planner_stages: Vec<ToolPlannerStage>,
364}
365
366impl Default for ToolExposurePolicy {
367 fn default() -> Self {
368 Self {
369 exposure_mode: ToolExposureMode::Auto,
370 allow_parallel_calls: false,
371 max_calls_per_turn: 1,
372 allowed_planner_stages: Vec::new(),
373 }
374 }
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct ToolDescriptor {
379 pub name: String,
380 pub version: String,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub description: Option<String>,
383 pub backend_kind: ToolBackendKind,
384 pub input_schema: Value,
385 pub output_mode: ToolOutputMode,
386 pub read_only: bool,
387 pub side_effect_class: ToolSideEffectClass,
388 pub idempotency_class: ToolIdempotencyClass,
389 pub approval_kind: ToolApprovalKind,
390 pub timeout_ms: u64,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub concurrency_key: Option<String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub cache_ttl_ms: Option<u64>,
395 pub exposure_mode: ToolExposureMode,
396 pub mcp_surface_kind: McpSurfaceKind,
397 #[serde(default)]
398 pub exposure_policy: ToolExposurePolicy,
399 #[serde(default)]
400 pub receipt_persistence: ToolReceiptPersistence,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub output_size_limit_bytes: Option<usize>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub provider_payload: Option<Value>,
405}
406
407impl ToolDescriptor {
408 pub fn supports_local_dispatch(&self) -> bool {
410 matches!(
411 self.backend_kind,
412 ToolBackendKind::LocalFunction
413 | ToolBackendKind::OpenAiFunction
414 | ToolBackendKind::OllamaFunction
415 )
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct ToolBudgetContext {
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub budget_kind: Option<String>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub max_steps: Option<u32>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub time_budget_ms: Option<u64>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub cost_budget_units: Option<u64>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct ToolCtx {
433 pub trace_ctx: TraceCtx,
434 pub attempt_id: AttemptId,
435 pub trial_id: TrialId,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub deadline: Option<String>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub workload_class: Option<String>,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub budget_context: Option<ToolBudgetContext>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub scope: Option<ScopeKey>,
444 #[serde(default)]
445 pub dry_run: bool,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub approval_grant: Option<ApprovalGrant>,
448 #[serde(default, skip_serializing, skip_deserializing)]
449 pub execution_permit: Option<ToolExecutionPermit>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub idempotency_key: Option<String>,
452 pub caller: String,
453 pub planner_stage: ToolPlannerStage,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub parent_receipt_id: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub family_receipt_id: Option<String>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub replay_parent_receipt_id: Option<String>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub remote_oracle_lease_id: Option<RemoteOracleLeaseId>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub remote_slice_result_id: Option<RemoteSliceResultId>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub attestation_envelope_id: Option<AttestationEnvelopeId>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub cross_runtime_replay_ticket_id: Option<CrossRuntimeReplayTicketId>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub retry_owner: Option<ToolRetryOwner>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct ToolCall {
474 pub descriptor_name: String,
475 pub descriptor_version: String,
476 pub arguments: Value,
477 pub origin_kind: ToolOriginKind,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub provider_call_id: Option<String>,
480 pub tool_run_id: String,
481}
482
483impl ToolCall {
484 pub fn new(
486 descriptor_name: impl Into<String>,
487 descriptor_version: impl Into<String>,
488 arguments: Value,
489 origin_kind: ToolOriginKind,
490 ) -> Self {
491 Self {
492 descriptor_name: descriptor_name.into(),
493 descriptor_version: descriptor_version.into(),
494 arguments,
495 origin_kind,
496 provider_call_id: None,
497 tool_run_id: uuid::Uuid::new_v4().to_string(),
498 }
499 }
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ToolArtifactRef {
504 pub artifact_id: ArtifactId,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub label: Option<String>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub mime_type: Option<String>,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ToolJobHandle {
513 pub job_id: String,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub status: Option<String>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct ToolResult {
520 pub mode: ToolOutputMode,
521 pub payload: Value,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub display_text: Option<String>,
524}
525
526impl ToolResult {
527 pub fn json(payload: Value) -> Self {
529 Self {
530 mode: ToolOutputMode::StructuredJson,
531 payload,
532 display_text: None,
533 }
534 }
535
536 pub fn text(text: impl Into<String>) -> Self {
538 let text = text.into();
539 Self {
540 mode: ToolOutputMode::Text,
541 payload: Value::String(text.clone()),
542 display_text: Some(text),
543 }
544 }
545
546 pub fn artifact_refs(refs: Vec<ToolArtifactRef>) -> Self {
548 Self {
549 mode: ToolOutputMode::ArtifactRefs,
550 payload: serde_json::to_value(refs).unwrap_or(Value::Array(Vec::new())),
551 display_text: None,
552 }
553 }
554
555 pub fn job_handle(handle: ToolJobHandle) -> Self {
557 Self {
558 mode: ToolOutputMode::JobHandle,
559 payload: serde_json::to_value(handle).unwrap_or(Value::Null),
560 display_text: None,
561 }
562 }
563
564 pub fn to_model_output(&self) -> String {
566 match self.mode {
567 ToolOutputMode::Text => self
568 .display_text
569 .clone()
570 .unwrap_or_else(|| self.payload.as_str().unwrap_or_default().to_string()),
571 ToolOutputMode::StructuredJson
572 | ToolOutputMode::ArtifactRefs
573 | ToolOutputMode::JobHandle => serde_json::to_string(&self.payload)
574 .unwrap_or_else(|_| "\"<tool_result_unserializable>\"".to_string()),
575 }
576 }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, Error)]
580#[error("{class:?}: {message}")]
581pub struct ToolError {
582 pub class: ToolErrorClass,
583 pub message: String,
584 #[serde(default)]
585 pub retryable: bool,
586 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub details: Option<Value>,
588}
589
590impl ToolError {
591 pub fn new(class: ToolErrorClass, message: impl Into<String>) -> Self {
593 Self {
594 class,
595 message: message.into(),
596 retryable: false,
597 details: None,
598 }
599 }
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct ToolReceipt {
604 pub receipt_id: String,
605 pub tool_name: String,
606 pub tool_version: String,
607 pub backend_kind: ToolBackendKind,
608 pub input_digest: ContentDigest,
609 pub output_digest_or_refs: Value,
610 pub policy_hash: ContentDigest,
611 pub approval_state: ToolApprovalState,
612 pub host_identity: String,
613 pub started_at: String,
614 pub finished_at: String,
615 pub trace_ctx: TraceCtx,
616 pub attempt_id: AttemptId,
617 pub trial_id: TrialId,
618 pub planner_stage: ToolPlannerStage,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub deadline: Option<String>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub workload_class: Option<String>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub budget_context: Option<ToolBudgetContext>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub parent_receipt_id: Option<String>,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub family_receipt_id: Option<String>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub replay_parent_receipt_id: Option<String>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub remote_oracle_lease_id: Option<RemoteOracleLeaseId>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub remote_slice_result_id: Option<RemoteSliceResultId>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub attestation_envelope_id: Option<AttestationEnvelopeId>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub cross_runtime_replay_ticket_id: Option<CrossRuntimeReplayTicketId>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub error_class: Option<ToolErrorClass>,
641 pub retry_owner: ToolRetryOwner,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub replay_link: Option<String>,
644 pub tool_run_id: String,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub provider_call_id: Option<String>,
647}
648
649impl ToolReceipt {
650 pub fn to_forge_tool_receipt_v2(
652 &self,
653 raw_payload: Value,
654 ) -> semantic_memory_forge::ForgeToolReceiptV2 {
655 semantic_memory_forge::ForgeToolReceiptV2 {
656 schema_version: semantic_memory_forge::FORGE_TOOL_RECEIPT_V2_SCHEMA.into(),
657 receipt_id: self.receipt_id.clone(),
658 tool_run_id: self.tool_run_id.clone(),
659 tool_name: self.tool_name.clone(),
660 tool_version: self.tool_version.clone(),
661 backend_kind: self.backend_kind.as_str().into(),
662 input_digest: self.input_digest.clone(),
663 output_digest_or_refs: self.output_digest_or_refs.clone(),
664 policy_hash: self.policy_hash.clone(),
665 approval_state: self.approval_state.as_str().into(),
666 host_identity: self.host_identity.clone(),
667 started_at: self.started_at.clone(),
668 finished_at: self.finished_at.clone(),
669 trace_ctx: self.trace_ctx.clone(),
670 attempt_id: self.attempt_id.clone(),
671 trial_id: self.trial_id.clone(),
672 planner_stage: self.planner_stage.as_str().into(),
673 deadline: self.deadline.clone(),
674 workload_class: self.workload_class.clone(),
675 budget_context: self.budget_context.as_ref().map(|budget| {
676 semantic_memory_forge::ForgeToolBudgetContext {
677 budget_kind: budget.budget_kind.clone(),
678 max_steps: budget.max_steps,
679 time_budget_ms: budget.time_budget_ms,
680 cost_budget_units: budget.cost_budget_units,
681 }
682 }),
683 parent_receipt_id: self.parent_receipt_id.clone(),
684 family_receipt_id: self.family_receipt_id.clone(),
685 replay_parent_receipt_id: self.replay_parent_receipt_id.clone(),
686 error_class: self.error_class.as_ref().map(|class| format!("{class:?}")),
687 retry_owner: self.retry_owner.as_str().into(),
688 replay_link: self.replay_link.clone(),
689 provider_call_id: self.provider_call_id.clone(),
690 raw_payload,
691 }
692 }
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct ToolEffectDispatchReceiptV1 {
697 pub schema_version: String,
698 pub tool_effect_dispatch_receipt_id: ToolEffectDispatchReceiptId,
699 pub effect_commit_decision_id: EffectCommitDecisionId,
700 pub tool_receipt_id: String,
701 pub provider_route: Vec<String>,
702 pub dispatch_state: String,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
704 pub effect_execution_receipt_id: Option<EffectExecutionReceiptId>,
705 pub deadline_at_dispatch: String,
706 pub cancellation_reason: String,
707}