Skip to content

Commit 46265f4

Browse files
authored
Consume remote Agent Mode context snapshots (warpdotdev#12669)
1 parent 002eb1c commit 46265f4

14 files changed

Lines changed: 1131 additions & 290 deletions

app/src/ai/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ pub(crate) mod metadata_project_rules;
4040
pub mod onboarding;
4141
pub(crate) mod persisted_workspace;
4242
pub(crate) mod predict;
43+
#[cfg(all(not(target_family = "wasm"), feature = "local_fs"))]
44+
pub(crate) mod remote_agent_context;
4345
pub(crate) mod remote_context_files;
4446
pub mod request_usage_model;
4547
pub(crate) mod restored_conversations;

app/src/ai/remote_agent_context.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
use ::ai::project_context::model::{ProjectContextModel, ProjectRule};
2+
use ::ai::skills::{
3+
get_provider_for_path, parse_skill_content_at_location, ParsedSkill, SkillProvider, SkillScope,
4+
};
5+
use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent};
6+
use remote_server::proto::{
7+
remote_skill_proto, RemoteAgentContextSnapshot, RemoteContextFileProto, RemoteSkillProto,
8+
};
9+
use warp_core::features::FeatureFlag;
10+
use warp_core::safe_warn;
11+
use warp_util::host_id::HostId;
12+
use warp_util::local_or_remote_path::LocalOrRemotePath;
13+
use warp_util::remote_path::RemotePath;
14+
use warp_util::standardized_path::StandardizedPath;
15+
use warpui::{Entity, ModelContext, SingletonEntity};
16+
17+
use super::mcp::McpIntegration;
18+
use super::skills::{BundledSkill, BundledSkillActivation, SkillManager};
19+
20+
/// Home skills parsed from a remote agent context snapshot.
21+
struct HomeSkills {
22+
home_dir: LocalOrRemotePath,
23+
skills: Vec<ParsedSkill>,
24+
}
25+
26+
/// Valid application state parsed from a remote agent context snapshot.
27+
struct RemoteAgentContextState {
28+
bundled_skills: Option<BundledSkill>,
29+
home_skills: Option<HomeSkills>,
30+
global_rules: Vec<ProjectRule>,
31+
}
32+
33+
pub(crate) struct RemoteAgentContext;
34+
35+
impl RemoteAgentContext {
36+
pub(crate) fn new(ctx: &mut ModelContext<Self>) -> Self {
37+
let remote_server_manager = RemoteServerManager::handle(ctx);
38+
ctx.subscribe_to_model(&remote_server_manager, |me, _, event, ctx| {
39+
if let RemoteServerManagerEvent::RemoteAgentContextSnapshot { host_id, snapshot } =
40+
event
41+
{
42+
me.reconcile_snapshot(host_id.clone(), snapshot.clone(), ctx);
43+
return;
44+
}
45+
if let RemoteServerManagerEvent::HostDisconnected { host_id } = event {
46+
me.remove_host_context(host_id, ctx);
47+
}
48+
});
49+
Self
50+
}
51+
52+
fn reconcile_snapshot(
53+
&mut self,
54+
host_id: HostId,
55+
snapshot: RemoteAgentContextSnapshot,
56+
ctx: &mut ModelContext<Self>,
57+
) {
58+
let RemoteAgentContextState {
59+
bundled_skills,
60+
home_skills,
61+
global_rules,
62+
} = parse_snapshot(&host_id, snapshot);
63+
SkillManager::handle(ctx).update(ctx, |manager, _| {
64+
manager.replace_remote_agent_context(
65+
host_id.clone(),
66+
bundled_skills,
67+
home_skills.map(|home| (home.home_dir, home.skills)),
68+
);
69+
});
70+
ProjectContextModel::handle(ctx).update(ctx, |model, _| {
71+
model.set_remote_global_rules(host_id, global_rules);
72+
});
73+
}
74+
75+
fn remove_host_context(&mut self, host_id: &HostId, ctx: &mut ModelContext<Self>) {
76+
SkillManager::handle(ctx).update(ctx, |manager, _| {
77+
manager.remove_remote_agent_context(host_id);
78+
});
79+
ProjectContextModel::handle(ctx).update(ctx, |model, _| {
80+
model.remove_remote_global_rules(host_id);
81+
});
82+
}
83+
}
84+
85+
fn parse_snapshot(
86+
host_id: &HostId,
87+
snapshot: RemoteAgentContextSnapshot,
88+
) -> RemoteAgentContextState {
89+
let bundled_skills = FeatureFlag::BundledSkills
90+
.is_enabled()
91+
.then(|| bundled_skill_from_protos(host_id, &snapshot.skills));
92+
let Some(home_dir) = remote_path(host_id, &snapshot.home_dir) else {
93+
safe_warn!(
94+
safe: ("Ignoring remote home context with an invalid home directory"),
95+
full: ("Ignoring remote home context with an invalid home directory for {host_id}")
96+
);
97+
return RemoteAgentContextState {
98+
bundled_skills,
99+
home_skills: None,
100+
global_rules: Vec::new(),
101+
};
102+
};
103+
let skills = snapshot
104+
.skills
105+
.iter()
106+
.filter(|proto| matches!(proto.source, Some(remote_skill_proto::Source::Home(_))))
107+
.filter_map(|proto| {
108+
parse_remote_skill(
109+
host_id,
110+
proto,
111+
SkillScope::Home,
112+
Some(&home_dir),
113+
get_provider_for_path,
114+
)
115+
})
116+
.collect();
117+
let global_rules = snapshot
118+
.global_rules
119+
.into_iter()
120+
.filter_map(|file| project_rule_within_home(host_id, file, &home_dir))
121+
.collect();
122+
RemoteAgentContextState {
123+
bundled_skills,
124+
home_skills: Some(HomeSkills { home_dir, skills }),
125+
global_rules,
126+
}
127+
}
128+
129+
fn parse_remote_skill(
130+
host_id: &HostId,
131+
proto: &RemoteSkillProto,
132+
scope: SkillScope,
133+
required_root: Option<&LocalOrRemotePath>,
134+
provider_for_path: impl FnOnce(&LocalOrRemotePath) -> Option<SkillProvider>,
135+
) -> Option<ParsedSkill> {
136+
let Some(path) = remote_path(host_id, &proto.path) else {
137+
safe_warn!(
138+
safe: ("Skipping remote skill with an invalid path"),
139+
full: ("Skipping remote skill with an invalid path: {}", proto.path)
140+
);
141+
return None;
142+
};
143+
if required_root.is_some_and(|root| !path.starts_with(root)) {
144+
return None;
145+
}
146+
let provider = provider_for_path(&path)?;
147+
match parse_skill_content_at_location(path, &proto.content, provider, scope) {
148+
Ok(skill) => Some(skill),
149+
Err(err) => {
150+
safe_warn!(
151+
safe: ("Skipping remote skill that failed to parse"),
152+
full: ("Skipping remote skill at {} that failed to parse: {err:#}", proto.path)
153+
);
154+
None
155+
}
156+
}
157+
}
158+
159+
fn bundled_skill_from_protos(host_id: &HostId, skills: &[RemoteSkillProto]) -> BundledSkill {
160+
let definitions = skills.iter().filter_map(|proto| {
161+
let remote_skill_proto::Source::Bundled(metadata) = proto.source.as_ref()? else {
162+
return None;
163+
};
164+
let skill = parse_remote_skill(
165+
host_id,
166+
proto,
167+
SkillScope::Bundled,
168+
None,
169+
|_| Some(SkillProvider::Warp),
170+
)?;
171+
let activation = match metadata.requires_mcp.as_deref() {
172+
None => BundledSkillActivation::Always,
173+
Some(wire_id) => match mcp_integration_from_wire_id(wire_id) {
174+
Some(integration) => BundledSkillActivation::RequiresMcp(integration),
175+
None => {
176+
safe_warn!(
177+
safe: ("Skipping bundled skill with an unknown MCP integration"),
178+
full: ("Skipping bundled skill {} with an unknown MCP integration: {wire_id}", metadata.id)
179+
);
180+
return None;
181+
}
182+
},
183+
};
184+
Some((metadata.id.clone(), skill, activation))
185+
});
186+
BundledSkill::from_definitions(definitions)
187+
}
188+
189+
fn mcp_integration_from_wire_id(wire_id: &str) -> Option<McpIntegration> {
190+
match wire_id {
191+
"figma" => Some(McpIntegration::Figma),
192+
_ => None,
193+
}
194+
}
195+
196+
fn remote_path(host_id: &HostId, path: &str) -> Option<LocalOrRemotePath> {
197+
StandardizedPath::try_new(path)
198+
.ok()
199+
.map(|path| LocalOrRemotePath::Remote(RemotePath::new(host_id.clone(), path)))
200+
}
201+
202+
fn project_rule_within_home(
203+
host_id: &HostId,
204+
file: RemoteContextFileProto,
205+
home_dir: &LocalOrRemotePath,
206+
) -> Option<ProjectRule> {
207+
let path = remote_path(host_id, &file.path)?;
208+
path.starts_with(home_dir).then_some(ProjectRule {
209+
path,
210+
content: file.content,
211+
})
212+
}
213+
214+
impl Entity for RemoteAgentContext {
215+
type Event = ();
216+
}
217+
218+
impl SingletonEntity for RemoteAgentContext {}
219+
220+
#[cfg(test)]
221+
#[path = "remote_agent_context_tests.rs"]
222+
mod tests;

0 commit comments

Comments
 (0)