Skip to main content

agent_client_protocol_schema/v2/
plan.rs

1//! Execution plans for complex tasks that require multiple steps.
2//!
3//! Plans are strategies that agents share with clients through session updates,
4//! providing real-time visibility into their thinking and progress.
5//!
6//! See: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)
7
8use std::{collections::BTreeMap, sync::Arc};
9
10use derive_more::{Display, From};
11use schemars::JsonSchema;
12use schemars::Schema;
13use serde::{Deserialize, Serialize};
14use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
15
16use super::Meta;
17use crate::{IntoOption, SkipListener};
18
19/// Unique identifier for a plan within a session.
20#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
21#[serde(transparent)]
22#[from(Arc<str>, String, &'static str)]
23#[non_exhaustive]
24pub struct PlanId(pub Arc<str>);
25
26impl PlanId {
27    /// Wraps a protocol string as a typed [`PlanId`].
28    #[must_use]
29    pub fn new(id: impl Into<Arc<str>>) -> Self {
30        Self(id.into())
31    }
32}
33
34/// A content update for a plan identified by ID.
35#[serde_as]
36#[skip_serializing_none]
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
38#[serde(rename_all = "camelCase")]
39#[non_exhaustive]
40pub struct PlanUpdate {
41    /// The updated plan content.
42    pub plan: PlanUpdateContent,
43    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
44    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
45    /// these keys.
46    ///
47    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
48    #[serde_as(deserialize_as = "DefaultOnError")]
49    #[schemars(extend("x-deserialize-default-on-error" = true))]
50    #[serde(default)]
51    #[serde(rename = "_meta")]
52    pub meta: Option<Meta>,
53}
54
55impl PlanUpdate {
56    /// Builds [`PlanUpdate`] with the required fields set; optional fields start unset or empty.
57    #[must_use]
58    pub fn new(plan: PlanUpdateContent) -> Self {
59        Self { plan, meta: None }
60    }
61
62    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
63    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
64    /// these keys.
65    ///
66    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
67    #[must_use]
68    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
69        self.meta = meta.into_option();
70        self
71    }
72}
73
74/// Updated content for a plan.
75#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
76#[serde(tag = "type", rename_all = "snake_case")]
77#[schemars(extend("discriminator" = {"propertyName": "type"}))]
78#[non_exhaustive]
79pub enum PlanUpdateContent {
80    /// Structured plan entries.
81    Items(PlanItems),
82    /// **UNSTABLE**
83    ///
84    /// This capability is not part of the spec yet, and may be removed or changed at any point.
85    ///
86    /// A URI pointing to a file containing the plan.
87    #[cfg(feature = "unstable_plan_operations")]
88    File(PlanFile),
89    /// **UNSTABLE**
90    ///
91    /// This capability is not part of the spec yet, and may be removed or changed at any point.
92    ///
93    /// Raw markdown content for the plan.
94    #[cfg(feature = "unstable_plan_operations")]
95    Markdown(PlanMarkdown),
96    /// Custom or future plan update content.
97    ///
98    /// Values beginning with `_` are reserved for implementation-specific
99    /// extensions. Unknown values that do not begin with `_` are reserved for
100    /// future ACP variants.
101    ///
102    /// Receivers that do not understand this content type should preserve the
103    /// raw payload when storing, replaying, proxying, or forwarding plans, and
104    /// otherwise ignore it or display it generically.
105    #[serde(untagged)]
106    Other(OtherPlanUpdateContent),
107}
108
109/// Custom or future plan update content payload.
110#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
111#[schemars(inline)]
112#[schemars(transform = other_plan_update_content_schema)]
113#[serde(rename_all = "camelCase")]
114#[non_exhaustive]
115pub struct OtherPlanUpdateContent {
116    /// Custom or future plan update content type.
117    ///
118    /// Values beginning with `_` are reserved for implementation-specific
119    /// extensions. Unknown values that do not begin with `_` are reserved for
120    /// future ACP variants.
121    #[serde(rename = "type")]
122    pub type_: String,
123    /// The plan ID to update.
124    pub id: PlanId,
125    /// Additional fields from the unknown plan update content payload.
126    #[serde(flatten)]
127    pub fields: BTreeMap<String, serde_json::Value>,
128}
129
130impl OtherPlanUpdateContent {
131    /// Builds [`OtherPlanUpdateContent`] from an unknown discriminator and preserves the remaining extension fields.
132    #[must_use]
133    pub fn new(
134        type_: impl Into<String>,
135        id: impl Into<PlanId>,
136        mut fields: BTreeMap<String, serde_json::Value>,
137    ) -> Self {
138        fields.remove("type");
139        fields.remove("id");
140        Self {
141            type_: type_.into(),
142            id: id.into(),
143            fields,
144        }
145    }
146}
147
148impl<'de> Deserialize<'de> for OtherPlanUpdateContent {
149    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150    where
151        D: serde::Deserializer<'de>,
152    {
153        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
154        let type_ = fields
155            .remove("type")
156            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
157        let serde_json::Value::String(type_) = type_ else {
158            return Err(serde::de::Error::custom("`type` must be a string"));
159        };
160        let id = fields
161            .remove("id")
162            .ok_or_else(|| serde::de::Error::missing_field("id"))?;
163        let serde_json::Value::String(id) = id else {
164            return Err(serde::de::Error::custom("`id` must be a string"));
165        };
166
167        if is_known_plan_update_content_type(&type_) {
168            return Err(serde::de::Error::custom(format!(
169                "known plan update content `{type_}` did not match its schema"
170            )));
171        }
172
173        Ok(Self {
174            type_,
175            id: PlanId::new(id),
176            fields,
177        })
178    }
179}
180
181fn is_known_plan_update_content_type(type_: &str) -> bool {
182    KNOWN_PLAN_UPDATE_CONTENT_TYPES.contains(&type_)
183}
184
185fn other_plan_update_content_schema(schema: &mut Schema) {
186    super::schema_util::reject_known_string_discriminators(
187        schema,
188        "type",
189        KNOWN_PLAN_UPDATE_CONTENT_TYPES,
190    );
191}
192
193const KNOWN_PLAN_UPDATE_CONTENT_TYPES: &[&str] = &["items", "file", "markdown"];
194
195impl PlanUpdateContent {
196    /// Builds a plan update that replaces the itemized entries for a plan.
197    #[must_use]
198    pub fn items(id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
199        Self::Items(PlanItems::new(id, entries))
200    }
201
202    /// Builds a plan update that points clients at an external plan file URI.
203    #[cfg(feature = "unstable_plan_operations")]
204    #[must_use]
205    pub fn file(id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
206        Self::File(PlanFile::new(id, uri))
207    }
208
209    /// Builds a plan update whose plan content is inline Markdown.
210    #[cfg(feature = "unstable_plan_operations")]
211    #[must_use]
212    pub fn markdown(id: impl Into<PlanId>, content: impl Into<String>) -> Self {
213        Self::Markdown(PlanMarkdown::new(id, content))
214    }
215}
216
217/// A plan represented as structured entries.
218#[serde_as]
219#[skip_serializing_none]
220#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
221#[serde(rename_all = "camelCase")]
222#[non_exhaustive]
223pub struct PlanItems {
224    /// The plan ID to update.
225    pub id: PlanId,
226    /// The list of tasks to be accomplished.
227    ///
228    /// When updating an item-based plan, the agent must send a complete list of all entries
229    /// with their current status. The client replaces that plan with each update.
230    #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
231    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
232    pub entries: Vec<PlanEntry>,
233    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
234    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
235    /// these keys.
236    ///
237    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
238    #[serde_as(deserialize_as = "DefaultOnError")]
239    #[schemars(extend("x-deserialize-default-on-error" = true))]
240    #[serde(default)]
241    #[serde(rename = "_meta")]
242    pub meta: Option<Meta>,
243}
244
245impl PlanItems {
246    /// Builds [`PlanItems`] with the required fields set; optional fields start unset or empty.
247    #[must_use]
248    pub fn new(id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
249        Self {
250            id: id.into(),
251            entries,
252            meta: None,
253        }
254    }
255
256    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
257    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
258    /// these keys.
259    ///
260    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
261    #[must_use]
262    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
263        self.meta = meta.into_option();
264        self
265    }
266}
267
268/// **UNSTABLE**
269///
270/// This capability is not part of the spec yet, and may be removed or changed at any point.
271///
272/// A plan represented by a file URI.
273#[cfg(feature = "unstable_plan_operations")]
274#[serde_as]
275#[skip_serializing_none]
276#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
277#[serde(rename_all = "camelCase")]
278#[non_exhaustive]
279pub struct PlanFile {
280    /// The plan ID to update.
281    pub id: PlanId,
282    /// The URI of the file containing the plan.
283    pub uri: String,
284    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
285    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
286    /// these keys.
287    ///
288    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
289    #[serde_as(deserialize_as = "DefaultOnError")]
290    #[schemars(extend("x-deserialize-default-on-error" = true))]
291    #[serde(default)]
292    #[serde(rename = "_meta")]
293    pub meta: Option<Meta>,
294}
295
296#[cfg(feature = "unstable_plan_operations")]
297impl PlanFile {
298    /// Builds [`PlanFile`] with the required fields set; optional fields start unset or empty.
299    #[must_use]
300    pub fn new(id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
301        Self {
302            id: id.into(),
303            uri: uri.into(),
304            meta: None,
305        }
306    }
307
308    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
309    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
310    /// these keys.
311    ///
312    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
313    #[must_use]
314    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
315        self.meta = meta.into_option();
316        self
317    }
318}
319
320/// **UNSTABLE**
321///
322/// This capability is not part of the spec yet, and may be removed or changed at any point.
323///
324/// A plan represented as raw markdown content.
325#[cfg(feature = "unstable_plan_operations")]
326#[serde_as]
327#[skip_serializing_none]
328#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
329#[serde(rename_all = "camelCase")]
330#[non_exhaustive]
331pub struct PlanMarkdown {
332    /// The plan ID to update.
333    pub id: PlanId,
334    /// Markdown content for the plan.
335    pub content: String,
336    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
337    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
338    /// these keys.
339    ///
340    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
341    #[serde_as(deserialize_as = "DefaultOnError")]
342    #[schemars(extend("x-deserialize-default-on-error" = true))]
343    #[serde(default)]
344    #[serde(rename = "_meta")]
345    pub meta: Option<Meta>,
346}
347
348#[cfg(feature = "unstable_plan_operations")]
349impl PlanMarkdown {
350    /// Builds [`PlanMarkdown`] with the required fields set; optional fields start unset or empty.
351    #[must_use]
352    pub fn new(id: impl Into<PlanId>, content: impl Into<String>) -> Self {
353        Self {
354            id: id.into(),
355            content: content.into(),
356            meta: None,
357        }
358    }
359
360    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
361    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
362    /// these keys.
363    ///
364    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
365    #[must_use]
366    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
367        self.meta = meta.into_option();
368        self
369    }
370}
371
372/// **UNSTABLE**
373///
374/// This capability is not part of the spec yet, and may be removed or changed at any point.
375///
376/// Removal notice for a plan identified by ID.
377#[cfg(feature = "unstable_plan_operations")]
378#[serde_as]
379#[skip_serializing_none]
380#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
381#[serde(rename_all = "camelCase")]
382#[non_exhaustive]
383pub struct PlanRemoved {
384    /// The plan ID to remove.
385    pub id: PlanId,
386    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
387    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
388    /// these keys.
389    ///
390    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
391    #[serde_as(deserialize_as = "DefaultOnError")]
392    #[schemars(extend("x-deserialize-default-on-error" = true))]
393    #[serde(default)]
394    #[serde(rename = "_meta")]
395    pub meta: Option<Meta>,
396}
397
398#[cfg(feature = "unstable_plan_operations")]
399impl PlanRemoved {
400    /// Builds [`PlanRemoved`] with the required fields set; optional fields start unset or empty.
401    #[must_use]
402    pub fn new(id: impl Into<PlanId>) -> Self {
403        Self {
404            id: id.into(),
405            meta: None,
406        }
407    }
408
409    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
410    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
411    /// these keys.
412    ///
413    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
414    #[must_use]
415    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
416        self.meta = meta.into_option();
417        self
418    }
419}
420
421/// A single entry in the execution plan.
422///
423/// Represents a task or goal that the assistant intends to accomplish
424/// as part of fulfilling the user's request.
425/// See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
426#[serde_as]
427#[skip_serializing_none]
428#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
429#[serde(rename_all = "camelCase")]
430#[non_exhaustive]
431pub struct PlanEntry {
432    /// Human-readable description of what this task aims to accomplish.
433    pub content: String,
434    /// The relative importance of this task.
435    /// Used to indicate which tasks are most critical to the overall goal.
436    pub priority: PlanEntryPriority,
437    /// Current execution status of this task.
438    pub status: PlanEntryStatus,
439    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
440    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
441    /// these keys.
442    ///
443    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
444    #[serde_as(deserialize_as = "DefaultOnError")]
445    #[schemars(extend("x-deserialize-default-on-error" = true))]
446    #[serde(default)]
447    #[serde(rename = "_meta")]
448    pub meta: Option<Meta>,
449}
450
451impl PlanEntry {
452    /// Builds [`PlanEntry`] with the required fields set; optional fields start unset or empty.
453    #[must_use]
454    pub fn new(
455        content: impl Into<String>,
456        priority: PlanEntryPriority,
457        status: PlanEntryStatus,
458    ) -> Self {
459        Self {
460            content: content.into(),
461            priority,
462            status,
463            meta: None,
464        }
465    }
466
467    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
468    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
469    /// these keys.
470    ///
471    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
472    #[must_use]
473    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
474        self.meta = meta.into_option();
475        self
476    }
477}
478
479/// Priority levels for plan entries.
480///
481/// Used to indicate the relative importance or urgency of different
482/// tasks in the execution plan.
483/// See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
484#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
485#[serde(rename_all = "snake_case")]
486#[non_exhaustive]
487pub enum PlanEntryPriority {
488    /// High priority task - critical to the overall goal.
489    High,
490    /// Medium priority task - important but not critical.
491    Medium,
492    /// Low priority task - nice to have but not essential.
493    Low,
494    /// Custom or future plan entry priority.
495    ///
496    /// Values beginning with `_` are reserved for implementation-specific
497    /// extensions. Unknown values that do not begin with `_` are reserved for
498    /// future ACP variants.
499    #[serde(untagged)]
500    Other(String),
501}
502
503/// Status of a plan entry in the execution flow.
504///
505/// Tracks the lifecycle of each task from planning through completion.
506/// See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
507#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509#[non_exhaustive]
510pub enum PlanEntryStatus {
511    /// The task has not started yet.
512    Pending,
513    /// The task is currently being worked on.
514    InProgress,
515    /// The task has been successfully completed.
516    Completed,
517    /// Custom or future plan entry status.
518    ///
519    /// Values beginning with `_` are reserved for implementation-specific
520    /// extensions. Unknown values that do not begin with `_` are reserved for
521    /// future ACP variants.
522    #[serde(untagged)]
523    Other(String),
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn plan_entry_priority_preserves_unknown_variant() {
532        let priority: PlanEntryPriority = serde_json::from_str("\"urgent\"").unwrap();
533        assert_eq!(priority, PlanEntryPriority::Other("urgent".to_string()));
534        assert_eq!(serde_json::to_value(&priority).unwrap(), "urgent");
535    }
536
537    #[test]
538    fn plan_entry_status_preserves_unknown_variant() {
539        let status: PlanEntryStatus = serde_json::from_str("\"blocked\"").unwrap();
540        assert_eq!(status, PlanEntryStatus::Other("blocked".to_string()));
541        assert_eq!(serde_json::to_value(&status).unwrap(), "blocked");
542    }
543
544    #[test]
545    fn plan_update_content_preserves_unknown_variant() {
546        let content: PlanUpdateContent = serde_json::from_value(serde_json::json!({
547            "type": "_timeline",
548            "id": "plan-1",
549            "events": []
550        }))
551        .unwrap();
552
553        let PlanUpdateContent::Other(unknown) = content else {
554            panic!("expected unknown plan update content");
555        };
556
557        assert_eq!(unknown.type_, "_timeline");
558        assert_eq!(unknown.id.to_string(), "plan-1");
559        assert!(!unknown.fields.contains_key("id"));
560        assert_eq!(
561            serde_json::to_value(PlanUpdateContent::Other(unknown)).unwrap(),
562            serde_json::json!({
563                "type": "_timeline",
564                "id": "plan-1",
565                "events": []
566            })
567        );
568    }
569
570    #[test]
571    fn plan_update_content_does_not_hide_malformed_known_variant() {
572        assert!(
573            serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
574                "type": "items"
575            }))
576            .is_err()
577        );
578        assert!(
579            serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
580                "type": "file",
581                "id": "plan-1"
582            }))
583            .is_err()
584        );
585        assert!(
586            serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
587                "type": "markdown",
588                "id": "plan-1"
589            }))
590            .is_err()
591        );
592    }
593
594    #[test]
595    fn plan_update_content_requires_id_for_unknown_variant() {
596        assert!(
597            serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
598                "type": "_timeline"
599            }))
600            .is_err()
601        );
602    }
603}