Skip to main content

conversation_api/execution/
facade.rs

1//! App Facade contract used by the canonical Agent implementation.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::execution::{ActionId, ExternalError, InvocationContext, OperationId};
8
9/// Canonical App Facade request selected by Agent-owned tool code.
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct FacadeRequest {
13    /// Stable idempotency identifier for this tool operation.
14    pub operation_id: OperationId,
15    /// Canonical `/app/...` target. The Agent owns every tool-to-target mapping.
16    pub target: String,
17    /// Structured input validated and normalized by Agent-owned tool code.
18    pub input: Value,
19}
20
21pub use crate::{
22    ClientTask, InteractionKind, InteractionPreview, InteractionTone, PreviewDetail, PreviewValue,
23};
24
25/// Prepared operation awaiting an explicit commit or rejection.
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct PreparedAction {
29    /// Stable action identifier used for idempotent commit and rejection.
30    pub action_id: ActionId,
31    /// Idempotency identifier supplied by Runtime.
32    pub operation_id: OperationId,
33    /// App Facade-generated preview suitable for user confirmation.
34    pub preview: InteractionPreview,
35    /// Opaque execution plan. Runtime persists it but never exposes it to the model or UI.
36    #[serde(default)]
37    pub payload: Value,
38    /// App Facade revision that must still be current when committing.
39    pub revision: u64,
40    /// Presentation/continuation kind for clients.
41    pub interaction_kind: InteractionKind,
42}
43
44/// Structured result returned by an App Facade operation.
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct FacadeResult {
48    /// App Facade result payload.
49    pub value: Value,
50    /// A new prepared continuation when committing a user action still requires user work.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub continuation: Option<PreparedAction>,
53    /// New prompt-context generation when this operation changed Agent-visible facts.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub context_invalidation: Option<crate::execution::ContextInvalidation>,
56}
57
58/// Environment-specific implementation of the App Facade boundary.
59///
60/// Implementations may execute in-process (`MeowCore`) or through RPC (agent-cloud/Lion), but they
61/// cannot supply prompts, tools, workflows, or model-facing schemas.
62#[async_trait]
63pub trait AppFacade: Send + Sync {
64    /// Executes a direct App Facade request.
65    async fn invoke(
66        &self,
67        context: &InvocationContext,
68        request: FacadeRequest,
69    ) -> Result<FacadeResult, ExternalError>;
70
71    /// Prepares a mutating operation without applying it.
72    async fn prepare(
73        &self,
74        context: &InvocationContext,
75        request: FacadeRequest,
76    ) -> Result<PreparedAction, ExternalError>;
77
78    /// Commits a prepared operation idempotently.
79    async fn commit(
80        &self,
81        context: &InvocationContext,
82        action: PreparedAction,
83    ) -> Result<FacadeResult, ExternalError>;
84
85    /// Rejects a prepared operation idempotently.
86    async fn reject(
87        &self,
88        context: &InvocationContext,
89        action: PreparedAction,
90    ) -> Result<(), ExternalError>;
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn typed_preview_rejects_incomplete_client_work() {
99        let mut preview = InteractionPreview::confirmation(
100            "integration.install",
101            "Integration required",
102            "Install it, then continue.",
103        );
104        preview.client_task = Some(ClientTask::InstallIntegration {
105            integration_id: String::new(),
106        });
107        assert!(!preview.is_valid());
108
109        preview.client_task = Some(ClientTask::InstallIntegration {
110            integration_id: "provider.example".to_owned(),
111        });
112        assert!(preview.is_valid());
113        let encoded = serde_json::to_value(preview).expect("serializes");
114        assert_eq!(encoded["clientTask"]["type"], "install_integration");
115        assert_eq!(encoded["clientTask"]["integrationId"], "provider.example");
116    }
117}