1mod agent_configuration;
2pub(crate) mod agent_connection_store;
3mod agent_diff;
4mod agent_model_selector;
5mod agent_panel;
6mod agent_registry_ui;
7mod branch_names;
8mod buffer_codegen;
9mod completion_provider;
10mod config_options;
11pub(crate) mod connection_view;
12mod context;
13mod context_server_configuration;
14mod entry_view_state;
15mod external_source_prompt;
16mod favorite_models;
17mod inline_assistant;
18mod inline_prompt_editor;
19mod language_model_selector;
20mod mention_set;
21mod message_editor;
22mod mode_selector;
23mod model_selector;
24mod model_selector_popover;
25mod profile_selector;
26pub mod sidebar;
27mod slash_command;
28mod slash_command_picker;
29mod terminal_codegen;
30mod terminal_inline_assistant;
31#[cfg(any(test, feature = "test-support"))]
32pub mod test_support;
33mod text_thread_editor;
34mod text_thread_history;
35mod thread_history;
36mod ui;
37
38use std::rc::Rc;
39use std::sync::Arc;
40
41use agent_client_protocol as acp;
42use agent_settings::{AgentProfileId, AgentSettings};
43use assistant_slash_command::SlashCommandRegistry;
44use client::Client;
45use command_palette_hooks::CommandPaletteFilter;
46use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
47use fs::Fs;
48use gpui::{Action, App, Context, Entity, SharedString, Window, actions};
49use language::{
50 LanguageRegistry,
51 language_settings::{AllLanguageSettings, EditPredictionProvider},
52};
53use language_model::{
54 ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
55};
56use project::DisableAiSettings;
57use prompt_store::PromptBuilder;
58use schemars::JsonSchema;
59use serde::{Deserialize, Serialize};
60use settings::{LanguageModelSelection, Settings as _, SettingsStore};
61use std::any::TypeId;
62use workspace::Workspace;
63
64use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
65pub use crate::agent_panel::{
66 AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate, WorktreeCreationStatus,
67};
68use crate::agent_registry_ui::AgentRegistryPage;
69pub use crate::inline_assistant::InlineAssistant;
70pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
71pub(crate) use connection_view::ConnectionView;
72pub use external_source_prompt::ExternalSourcePrompt;
73pub(crate) use mode_selector::ModeSelector;
74pub(crate) use model_selector::ModelSelector;
75pub(crate) use model_selector_popover::ModelSelectorPopover;
76pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
77pub(crate) use thread_history::*;
78use zed_actions;
79
80actions!(
81 agent,
82 [
83 /// Creates a new text-based conversation thread.
84 NewTextThread,
85 /// Toggles the menu to create new agent threads.
86 ToggleNewThreadMenu,
87 /// Toggles the selector for choosing where new threads start (current project or new worktree).
88 ToggleStartThreadInSelector,
89 /// Toggles the navigation menu for switching between threads and views.
90 ToggleNavigationMenu,
91 /// Toggles the options menu for agent settings and preferences.
92 ToggleOptionsMenu,
93 /// Toggles the profile or mode selector for switching between agent profiles.
94 ToggleProfileSelector,
95 /// Cycles through available session modes.
96 CycleModeSelector,
97 /// Cycles through favorited models in the ACP model selector.
98 CycleFavoriteModels,
99 /// Expands the message editor to full size.
100 ExpandMessageEditor,
101 /// Removes all thread history.
102 RemoveHistory,
103 /// Opens the conversation history view.
104 OpenHistory,
105 /// Adds a context server to the configuration.
106 AddContextServer,
107 /// Removes the currently selected thread.
108 RemoveSelectedThread,
109 /// Starts a chat conversation with follow-up enabled.
110 ChatWithFollow,
111 /// Cycles to the next inline assist suggestion.
112 CycleNextInlineAssist,
113 /// Cycles to the previous inline assist suggestion.
114 CyclePreviousInlineAssist,
115 /// Moves focus up in the interface.
116 FocusUp,
117 /// Moves focus down in the interface.
118 FocusDown,
119 /// Moves focus left in the interface.
120 FocusLeft,
121 /// Moves focus right in the interface.
122 FocusRight,
123 /// Opens the active thread as a markdown file.
124 OpenActiveThreadAsMarkdown,
125 /// Opens the agent diff view to review changes.
126 OpenAgentDiff,
127 /// Copies the current thread to the clipboard as JSON for debugging.
128 CopyThreadToClipboard,
129 /// Loads a thread from the clipboard JSON for debugging.
130 LoadThreadFromClipboard,
131 /// Keeps the current suggestion or change.
132 Keep,
133 /// Rejects the current suggestion or change.
134 Reject,
135 /// Rejects all suggestions or changes.
136 RejectAll,
137 /// Undoes the most recent reject operation, restoring the rejected changes.
138 UndoLastReject,
139 /// Keeps all suggestions or changes.
140 KeepAll,
141 /// Allow this operation only this time.
142 AllowOnce,
143 /// Allow this operation and remember the choice.
144 AllowAlways,
145 /// Reject this operation only this time.
146 RejectOnce,
147 /// Follows the agent's suggestions.
148 Follow,
149 /// Resets the trial upsell notification.
150 ResetTrialUpsell,
151 /// Resets the trial end upsell notification.
152 ResetTrialEndUpsell,
153 /// Opens the "Add Context" menu in the message editor.
154 OpenAddContextMenu,
155 /// Continues the current thread.
156 ContinueThread,
157 /// Interrupts the current generation and sends the message immediately.
158 SendImmediately,
159 /// Sends the next queued message immediately.
160 SendNextQueuedMessage,
161 /// Removes the first message from the queue (the next one to be sent).
162 RemoveFirstQueuedMessage,
163 /// Edits the first message in the queue (the next one to be sent).
164 EditFirstQueuedMessage,
165 /// Clears all messages from the queue.
166 ClearMessageQueue,
167 /// Opens the permission granularity dropdown for the current tool call.
168 OpenPermissionDropdown,
169 /// Toggles thinking mode for models that support extended thinking.
170 ToggleThinkingMode,
171 /// Cycles through available thinking effort levels for the current model.
172 CycleThinkingEffort,
173 /// Toggles the thinking effort selector menu open or closed.
174 ToggleThinkingEffortMenu,
175 /// Toggles fast mode for models that support it.
176 ToggleFastMode,
177 ]
178);
179
180/// Action to authorize a tool call with a specific permission option.
181/// This is used by the permission granularity dropdown to authorize tool calls.
182#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
183#[action(namespace = agent)]
184#[serde(deny_unknown_fields)]
185pub struct AuthorizeToolCall {
186 /// The tool call ID to authorize.
187 pub tool_call_id: String,
188 /// The permission option ID to use.
189 pub option_id: String,
190 /// The kind of permission option (serialized as string).
191 pub option_kind: String,
192}
193
194/// Creates a new conversation thread, optionally based on an existing thread.
195#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
196#[action(namespace = agent)]
197#[serde(deny_unknown_fields)]
198pub struct NewThread;
199
200/// Creates a new external agent conversation thread.
201#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
202#[action(namespace = agent)]
203#[serde(deny_unknown_fields)]
204pub struct NewExternalAgentThread {
205 /// Which agent to use for the conversation.
206 agent: Option<ExternalAgent>,
207}
208
209#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
210#[action(namespace = agent)]
211#[serde(deny_unknown_fields)]
212pub struct NewNativeAgentThreadFromSummary {
213 from_session_id: agent_client_protocol::SessionId,
214}
215
216// TODO unify this with AgentType
217#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
218#[serde(rename_all = "snake_case")]
219pub enum ExternalAgent {
220 NativeAgent,
221 Custom { name: SharedString },
222}
223
224impl ExternalAgent {
225 pub fn server(
226 &self,
227 fs: Arc<dyn fs::Fs>,
228 thread_store: Entity<agent::ThreadStore>,
229 ) -> Rc<dyn agent_servers::AgentServer> {
230 match self {
231 Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
232 Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
233 }
234 }
235}
236
237/// Sets where new threads will run.
238#[derive(
239 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action,
240)]
241#[action(namespace = agent)]
242#[serde(rename_all = "snake_case", tag = "kind")]
243pub enum StartThreadIn {
244 #[default]
245 LocalProject,
246 NewWorktree,
247}
248
249/// Content to initialize new external agent with.
250pub enum AgentInitialContent {
251 ThreadSummary {
252 session_id: acp::SessionId,
253 title: Option<SharedString>,
254 },
255 ContentBlock {
256 blocks: Vec<agent_client_protocol::ContentBlock>,
257 auto_submit: bool,
258 },
259 FromExternalSource(ExternalSourcePrompt),
260}
261
262impl From<ExternalSourcePrompt> for AgentInitialContent {
263 fn from(prompt: ExternalSourcePrompt) -> Self {
264 Self::FromExternalSource(prompt)
265 }
266}
267
268/// Opens the profile management interface for configuring agent tools and settings.
269#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
270#[action(namespace = agent)]
271#[serde(deny_unknown_fields)]
272pub struct ManageProfiles {
273 #[serde(default)]
274 pub customize_tools: Option<AgentProfileId>,
275}
276
277impl ManageProfiles {
278 pub fn customize_tools(profile_id: AgentProfileId) -> Self {
279 Self {
280 customize_tools: Some(profile_id),
281 }
282 }
283}
284
285#[derive(Clone)]
286pub(crate) enum ModelUsageContext {
287 InlineAssistant,
288}
289
290impl ModelUsageContext {
291 pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
292 match self {
293 Self::InlineAssistant => {
294 LanguageModelRegistry::read_global(cx).inline_assistant_model()
295 }
296 }
297 }
298}
299
300/// Initializes the `agent` crate.
301pub fn init(
302 fs: Arc<dyn Fs>,
303 client: Arc<Client>,
304 prompt_builder: Arc<PromptBuilder>,
305 language_registry: Arc<LanguageRegistry>,
306 is_eval: bool,
307 cx: &mut App,
308) {
309 agent::ThreadStore::init_global(cx);
310 assistant_text_thread::init(client, cx);
311 rules_library::init(cx);
312 if !is_eval {
313 // Initializing the language model from the user settings messes with the eval, so we only initialize them when
314 // we're not running inside of the eval.
315 init_language_model_settings(cx);
316 }
317 assistant_slash_command::init(cx);
318 agent_panel::init(cx);
319 context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
320 TextThreadEditor::init(cx);
321
322 register_slash_commands(cx);
323 inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
324 terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
325 cx.observe_new(move |workspace, window, cx| {
326 ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
327 })
328 .detach();
329 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
330 workspace.register_action(
331 move |workspace: &mut Workspace,
332 _: &zed_actions::AcpRegistry,
333 window: &mut Window,
334 cx: &mut Context<Workspace>| {
335 let existing = workspace
336 .active_pane()
337 .read(cx)
338 .items()
339 .find_map(|item| item.downcast::<AgentRegistryPage>());
340
341 if let Some(existing) = existing {
342 existing.update(cx, |_, cx| {
343 project::AgentRegistryStore::global(cx)
344 .update(cx, |store, cx| store.refresh(cx));
345 });
346 workspace.activate_item(&existing, true, true, window, cx);
347 } else {
348 let registry_page = AgentRegistryPage::new(workspace, window, cx);
349 workspace.add_item_to_active_pane(
350 Box::new(registry_page),
351 None,
352 true,
353 window,
354 cx,
355 );
356 }
357 },
358 );
359 })
360 .detach();
361 cx.observe_new(ManageProfilesModal::register).detach();
362
363 // Update command palette filter based on AI settings
364 update_command_palette_filter(cx);
365
366 // Watch for settings changes
367 cx.observe_global::<SettingsStore>(|app_cx| {
368 // When settings change, update the command palette filter
369 update_command_palette_filter(app_cx);
370 })
371 .detach();
372
373 cx.on_flags_ready(|_, cx| {
374 update_command_palette_filter(cx);
375 })
376 .detach();
377}
378
379fn update_command_palette_filter(cx: &mut App) {
380 let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
381 let agent_enabled = AgentSettings::get_global(cx).enabled;
382 let agent_v2_enabled = cx.has_flag::<AgentV2FeatureFlag>();
383 let edit_prediction_provider = AllLanguageSettings::get_global(cx)
384 .edit_predictions
385 .provider;
386
387 CommandPaletteFilter::update_global(cx, |filter, _| {
388 use editor::actions::{
389 AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
390 NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
391 };
392 let edit_prediction_actions = [
393 TypeId::of::<AcceptEditPrediction>(),
394 TypeId::of::<AcceptNextWordEditPrediction>(),
395 TypeId::of::<AcceptNextLineEditPrediction>(),
396 TypeId::of::<AcceptEditPrediction>(),
397 TypeId::of::<ShowEditPrediction>(),
398 TypeId::of::<NextEditPrediction>(),
399 TypeId::of::<PreviousEditPrediction>(),
400 TypeId::of::<ToggleEditPrediction>(),
401 ];
402
403 if disable_ai {
404 filter.hide_namespace("agent");
405 filter.hide_namespace("agents");
406 filter.hide_namespace("assistant");
407 filter.hide_namespace("copilot");
408 filter.hide_namespace("zed_predict_onboarding");
409 filter.hide_namespace("edit_prediction");
410
411 filter.hide_action_types(&edit_prediction_actions);
412 filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
413 } else {
414 if agent_enabled {
415 filter.show_namespace("agent");
416 filter.show_namespace("agents");
417 filter.show_namespace("assistant");
418 } else {
419 filter.hide_namespace("agent");
420 filter.hide_namespace("agents");
421 filter.hide_namespace("assistant");
422 }
423
424 match edit_prediction_provider {
425 EditPredictionProvider::None => {
426 filter.hide_namespace("edit_prediction");
427 filter.hide_namespace("copilot");
428 filter.hide_action_types(&edit_prediction_actions);
429 }
430 EditPredictionProvider::Copilot => {
431 filter.show_namespace("edit_prediction");
432 filter.show_namespace("copilot");
433 filter.show_action_types(edit_prediction_actions.iter());
434 }
435 EditPredictionProvider::Zed
436 | EditPredictionProvider::Codestral
437 | EditPredictionProvider::Ollama
438 | EditPredictionProvider::OpenAiCompatibleApi
439 | EditPredictionProvider::Sweep
440 | EditPredictionProvider::Mercury
441 | EditPredictionProvider::Experimental(_) => {
442 filter.show_namespace("edit_prediction");
443 filter.hide_namespace("copilot");
444 filter.show_action_types(edit_prediction_actions.iter());
445 }
446 }
447
448 filter.show_namespace("zed_predict_onboarding");
449 filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
450 }
451
452 if agent_v2_enabled {
453 filter.show_namespace("multi_workspace");
454 } else {
455 filter.hide_namespace("multi_workspace");
456 }
457 });
458}
459
460fn init_language_model_settings(cx: &mut App) {
461 update_active_language_model_from_settings(cx);
462
463 cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
464 .detach();
465 cx.subscribe(
466 &LanguageModelRegistry::global(cx),
467 |_, event: &language_model::Event, cx| match event {
468 language_model::Event::ProviderStateChanged(_)
469 | language_model::Event::AddedProvider(_)
470 | language_model::Event::RemovedProvider(_)
471 | language_model::Event::ProvidersChanged => {
472 update_active_language_model_from_settings(cx);
473 }
474 _ => {}
475 },
476 )
477 .detach();
478}
479
480fn update_active_language_model_from_settings(cx: &mut App) {
481 let settings = AgentSettings::get_global(cx);
482
483 fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
484 language_model::SelectedModel {
485 provider: LanguageModelProviderId::from(selection.provider.0.clone()),
486 model: LanguageModelId::from(selection.model.clone()),
487 }
488 }
489
490 let default = settings.default_model.as_ref().map(to_selected_model);
491 let inline_assistant = settings
492 .inline_assistant_model
493 .as_ref()
494 .map(to_selected_model);
495 let commit_message = settings
496 .commit_message_model
497 .as_ref()
498 .map(to_selected_model);
499 let thread_summary = settings
500 .thread_summary_model
501 .as_ref()
502 .map(to_selected_model);
503 let inline_alternatives = settings
504 .inline_alternatives
505 .iter()
506 .map(to_selected_model)
507 .collect::<Vec<_>>();
508
509 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
510 registry.select_default_model(default.as_ref(), cx);
511 registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
512 registry.select_commit_message_model(commit_message.as_ref(), cx);
513 registry.select_thread_summary_model(thread_summary.as_ref(), cx);
514 registry.select_inline_alternative_models(inline_alternatives, cx);
515 });
516}
517
518fn register_slash_commands(cx: &mut App) {
519 let slash_command_registry = SlashCommandRegistry::global(cx);
520
521 slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
522 slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
523 slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
524 slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
525 slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
526 slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
527 slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
528 slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
529 slash_command_registry
530 .register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
531 slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
532
533 cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
534 move |is_enabled, _cx| {
535 if is_enabled {
536 slash_command_registry.register_command(
537 assistant_slash_commands::StreamingExampleSlashCommand,
538 false,
539 );
540 }
541 }
542 })
543 .detach();
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use agent_settings::{AgentProfileId, AgentSettings};
550 use command_palette_hooks::CommandPaletteFilter;
551 use editor::actions::AcceptEditPrediction;
552 use gpui::{BorrowAppContext, TestAppContext, px};
553 use project::DisableAiSettings;
554 use settings::{
555 DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
556 };
557
558 #[gpui::test]
559 fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
560 // Init settings
561 cx.update(|cx| {
562 let store = SettingsStore::test(cx);
563 cx.set_global(store);
564 command_palette_hooks::init(cx);
565 AgentSettings::register(cx);
566 DisableAiSettings::register(cx);
567 AllLanguageSettings::register(cx);
568 });
569
570 let agent_settings = AgentSettings {
571 enabled: true,
572 button: true,
573 dock: DockPosition::Right,
574 default_width: px(300.),
575 default_height: px(600.),
576 default_model: None,
577 inline_assistant_model: None,
578 inline_assistant_use_streaming_tools: false,
579 commit_message_model: None,
580 thread_summary_model: None,
581 inline_alternatives: vec![],
582 favorite_models: vec![],
583 default_profile: AgentProfileId::default(),
584 default_view: DefaultAgentView::Thread,
585 profiles: Default::default(),
586
587 notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
588 play_sound_when_agent_done: false,
589 single_file_review: false,
590 model_parameters: vec![],
591 enable_feedback: false,
592 expand_edit_card: true,
593 expand_terminal_card: true,
594 cancel_generation_on_terminal_stop: true,
595 use_modifier_to_send: true,
596 message_editor_min_lines: 1,
597 tool_permissions: Default::default(),
598 show_turn_stats: false,
599 };
600
601 cx.update(|cx| {
602 AgentSettings::override_global(agent_settings.clone(), cx);
603 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
604
605 // Initial update
606 update_command_palette_filter(cx);
607 });
608
609 // Assert visible
610 cx.update(|cx| {
611 let filter = CommandPaletteFilter::try_global(cx).unwrap();
612 assert!(
613 !filter.is_hidden(&NewThread),
614 "NewThread should be visible by default"
615 );
616 assert!(
617 !filter.is_hidden(&text_thread_editor::CopyCode),
618 "CopyCode should be visible when agent is enabled"
619 );
620 });
621
622 // Disable agent
623 cx.update(|cx| {
624 let mut new_settings = agent_settings.clone();
625 new_settings.enabled = false;
626 AgentSettings::override_global(new_settings, cx);
627
628 // Trigger update
629 update_command_palette_filter(cx);
630 });
631
632 // Assert hidden
633 cx.update(|cx| {
634 let filter = CommandPaletteFilter::try_global(cx).unwrap();
635 assert!(
636 filter.is_hidden(&NewThread),
637 "NewThread should be hidden when agent is disabled"
638 );
639 assert!(
640 filter.is_hidden(&text_thread_editor::CopyCode),
641 "CopyCode should be hidden when agent is disabled"
642 );
643 });
644
645 // Test EditPredictionProvider
646 // Enable EditPredictionProvider::Copilot
647 cx.update(|cx| {
648 cx.update_global::<SettingsStore, _>(|store, cx| {
649 store.update_user_settings(cx, |s| {
650 s.project
651 .all_languages
652 .edit_predictions
653 .get_or_insert(Default::default())
654 .provider = Some(EditPredictionProvider::Copilot);
655 });
656 });
657 update_command_palette_filter(cx);
658 });
659
660 cx.update(|cx| {
661 let filter = CommandPaletteFilter::try_global(cx).unwrap();
662 assert!(
663 !filter.is_hidden(&AcceptEditPrediction),
664 "EditPrediction should be visible when provider is Copilot"
665 );
666 });
667
668 // Disable EditPredictionProvider (None)
669 cx.update(|cx| {
670 cx.update_global::<SettingsStore, _>(|store, cx| {
671 store.update_user_settings(cx, |s| {
672 s.project
673 .all_languages
674 .edit_predictions
675 .get_or_insert(Default::default())
676 .provider = Some(EditPredictionProvider::None);
677 });
678 });
679 update_command_palette_filter(cx);
680 });
681
682 cx.update(|cx| {
683 let filter = CommandPaletteFilter::try_global(cx).unwrap();
684 assert!(
685 filter.is_hidden(&AcceptEditPrediction),
686 "EditPrediction should be hidden when provider is None"
687 );
688 });
689 }
690
691 #[test]
692 fn test_deserialize_external_agent_variants() {
693 assert_eq!(
694 serde_json::from_str::<ExternalAgent>(r#""native_agent""#).unwrap(),
695 ExternalAgent::NativeAgent,
696 );
697 assert_eq!(
698 serde_json::from_str::<ExternalAgent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
699 ExternalAgent::Custom {
700 name: "my-agent".into(),
701 },
702 );
703 }
704}