Skip to main content

systemprompt_models/mcp/
execution_source.rs

1//! Where a tool execution was observed, and how surely its rows were joined.
2//!
3//! Every tool call the platform records is seen from one of a fixed set of
4//! vantage points. [`ExecutionSource`] names that vantage point on the
5//! execution row so an operator can tell a server-observed run from a
6//! client-reported one. [`Correlation`] states whether the execution was
7//! joined to its intent and artifact by an exact key or by inference — an
8//! inferred join is a visible state, never a silent match.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16/// The vantage point the platform saw a tool result from.
17///
18/// In-process executor, the HTTP proxy tapping an external server, a
19/// `tool_result` block replayed in a `/v1/messages` history, or a client
20/// host's tool-completion hook (`PostToolUse` for Claude Code and Cowork;
21/// `OpenCode`'s own hook).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum ExecutionSource {
25    InProcess,
26    Proxy,
27    Gateway,
28    HookClaudeCode,
29    HookOpenCode,
30}
31
32impl ExecutionSource {
33    pub const ALL: [Self; 5] = [
34        Self::InProcess,
35        Self::Proxy,
36        Self::Gateway,
37        Self::HookClaudeCode,
38        Self::HookOpenCode,
39    ];
40
41    #[must_use]
42    pub const fn as_str(self) -> &'static str {
43        match self {
44            Self::InProcess => "in_process",
45            Self::Proxy => "proxy",
46            Self::Gateway => "gateway",
47            Self::HookClaudeCode => "hook_claude_code",
48            Self::HookOpenCode => "hook_opencode",
49        }
50    }
51
52    #[must_use]
53    pub fn parse(value: &str) -> Option<Self> {
54        Self::ALL.into_iter().find(|s| s.as_str() == value)
55    }
56
57    #[must_use]
58    pub const fn is_server_observed(self) -> bool {
59        matches!(self, Self::InProcess | Self::Proxy)
60    }
61
62    #[must_use]
63    pub const fn from_hook_host(host: &str) -> Self {
64        if host.eq_ignore_ascii_case("opencode") {
65            Self::HookOpenCode
66        } else {
67            Self::HookClaudeCode
68        }
69    }
70}
71
72impl std::fmt::Display for ExecutionSource {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.write_str(self.as_str())
75    }
76}
77
78/// How a tool result was joined to its invocation.
79///
80/// `Exact` by a key both sides carried (the client `tool_use_id` or the
81/// server `mcp_execution_id`); `Inferred` by session, tool name, payload
82/// digest and time — the only option when a client host dropped every id.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum Correlation {
86    Exact,
87    Inferred,
88}
89
90impl Correlation {
91    #[must_use]
92    pub const fn as_str(self) -> &'static str {
93        match self {
94            Self::Exact => "exact",
95            Self::Inferred => "inferred",
96        }
97    }
98
99    #[must_use]
100    pub fn parse(value: &str) -> Option<Self> {
101        match value {
102            "exact" => Some(Self::Exact),
103            "inferred" => Some(Self::Inferred),
104            _ => None,
105        }
106    }
107}
108
109impl std::fmt::Display for Correlation {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.write_str(self.as_str())
112    }
113}