1use std::collections::{BTreeMap, BTreeSet};
30
31use serde::{Deserialize, Serialize};
32
33use crate::events::{EventEnvelope, ThreadId, TurnId};
34use crate::extension::ProvidedService;
35use crate::inference::{AgentInferenceRequest, InferenceEvent, ModelDescriptor};
36use crate::tools::ToolSpec;
37
38mod dispatch;
39
40pub use dispatch::{
41 ProcessSubagentCancelParams, ProcessSubagentDefinitionsParams,
42 ProcessSubagentDefinitionsResult, ProcessSubagentDispatchAck, ProcessSubagentDispatchParams,
43 ProcessSubagentEvent, ProcessSubagentEventNotification, ProcessTaskCancelParams,
44 ProcessTaskEvent, ProcessTaskEventNotification, ProcessTaskExecuteAck,
45 ProcessTaskExecuteParams, ProcessTaskSpecParams, ProcessTaskSpecResult,
46};
47
48pub const PROCESS_EXTENSION_PROTOCOL_VERSION: &str = "0.2.0";
53
54pub const METHOD_INITIALIZE: &str = "extension/initialize";
55pub const METHOD_LIST_MODELS: &str = "inference/listModels";
56pub const METHOD_STREAM_TURN: &str = "inference/streamTurn";
57pub const METHOD_INFERENCE_EVENT: &str = "inference/event";
58pub const METHOD_SUBAGENTS_DEFINITIONS: &str = "subagents/definitions";
59pub const METHOD_SUBAGENTS_DISPATCH: &str = "subagents/dispatch";
60pub const METHOD_SUBAGENTS_EVENT: &str = "subagents/event";
61pub const METHOD_SUBAGENTS_CANCEL: &str = "subagents/cancel";
62pub const METHOD_TASKS_SPEC: &str = "tasks/spec";
63pub const METHOD_TASKS_EXECUTE: &str = "tasks/execute";
64pub const METHOD_TASKS_EVENT: &str = "tasks/event";
65pub const METHOD_TASKS_CANCEL: &str = "tasks/cancel";
66pub const METHOD_TOOLS_CALL: &str = "tools/call";
67pub const METHOD_EVENTS_HANDLE: &str = "events/handle";
68pub const METHOD_EXTENSION_EVENT: &str = "extension/event";
69pub const METHOD_SHUTDOWN: &str = "extension/shutdown";
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub struct ProcessExtensionConfig {
76 pub id: String,
77 #[serde(default = "default_enabled")]
78 pub enabled: bool,
79 pub manifest: String,
81 pub command: String,
82 #[serde(default)]
83 pub args: Vec<String>,
84 #[serde(default)]
85 pub cwd: Option<String>,
86 #[serde(default)]
87 pub env: BTreeMap<String, String>,
88 #[serde(default = "default_startup_timeout_ms")]
90 pub startup_timeout_ms: u64,
91 #[serde(default)]
93 pub event_filter: ProcessEventFilter,
94}
95
96fn default_enabled() -> bool {
97 true
98}
99
100fn default_startup_timeout_ms() -> u64 {
101 10_000
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
106pub struct ProcessEventFilter {
107 #[serde(default)]
108 pub kinds: Vec<String>,
109}
110
111impl ProcessEventFilter {
112 pub fn matches(&self, kind: &str) -> bool {
113 self.kinds.iter().any(|prefix| kind.starts_with(prefix))
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct ProcessExtensionManifest {
120 pub id: String,
121 pub name: String,
122 pub version: String,
123 pub api_version: String,
125 #[serde(default)]
126 pub description: Option<String>,
127 pub provides: Vec<ProcessProvidedService>,
128 #[serde(default)]
129 pub required_capabilities: Vec<String>,
130 #[serde(default)]
135 pub launch: Option<crate::packages::PackageExtensionLaunch>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143#[serde(rename_all = "snake_case", tag = "type")]
144pub enum ProcessProvidedService {
145 InferenceEngine { id: String },
146 EventSink { id: String },
147 SubagentDispatcher { id: String },
148 TaskExecutor { id: String },
149 ToolProvider { id: String, tools: Vec<ToolSpec> },
150}
151
152impl ProcessProvidedService {
153 pub fn service_id(&self) -> &str {
154 match self {
155 ProcessProvidedService::InferenceEngine { id } => id,
156 ProcessProvidedService::EventSink { id } => id,
157 ProcessProvidedService::SubagentDispatcher { id } => id,
158 ProcessProvidedService::TaskExecutor { id } => id,
159 ProcessProvidedService::ToolProvider { id, .. } => id,
160 }
161 }
162}
163
164impl From<&ProcessProvidedService> for ProvidedService {
165 fn from(service: &ProcessProvidedService) -> Self {
166 match service {
167 ProcessProvidedService::InferenceEngine { id } => {
168 ProvidedService::InferenceEngine(id.clone())
169 }
170 ProcessProvidedService::EventSink { id } => ProvidedService::EventSink(id.clone()),
171 ProcessProvidedService::SubagentDispatcher { id } => {
172 ProvidedService::SubagentDispatcher(id.clone())
173 }
174 ProcessProvidedService::TaskExecutor { id } => {
175 ProvidedService::TaskExecutor(id.clone())
176 }
177 ProcessProvidedService::ToolProvider { id, .. } => {
178 ProvidedService::ToolProvider(id.clone())
179 }
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186#[serde(rename_all = "camelCase")]
187pub struct ProcessInitializeParams {
188 pub protocol_version: String,
189 pub api_version: String,
190 pub extension_id: String,
191 pub cwd: String,
192 pub granted_capabilities: Vec<String>,
193 pub config: serde_json::Value,
195 pub event_filter: ProcessEventFilter,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200#[serde(rename_all = "camelCase")]
201pub struct ProcessInitializeResult {
202 pub protocol_version: String,
203 pub extension_id: String,
205 pub services: Vec<ProcessProvidedService>,
206 pub manifest_checksum: String,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(rename_all = "camelCase")]
213pub struct ProcessListModelsParams {
214 pub engine_id: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "camelCase")]
219pub struct ProcessListModelsResult {
220 pub models: Vec<ModelDescriptor>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225#[serde(rename_all = "camelCase")]
226pub struct ProcessStreamTurnParams {
227 pub engine_id: String,
228 pub stream_id: String,
229 pub thread_id: ThreadId,
230 pub turn_id: TurnId,
231 pub request: AgentInferenceRequest,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235#[serde(rename_all = "camelCase")]
236pub struct ProcessStreamTurnAck {
237 pub stream_id: String,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243#[serde(rename_all = "camelCase")]
244pub struct ProcessToolCallParams {
245 pub provider_id: String,
246 pub tool_name: String,
247 pub call_id: String,
248 pub thread_id: ThreadId,
249 pub turn_id: TurnId,
250 pub arguments: serde_json::Value,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
258#[serde(rename_all = "camelCase")]
259pub struct ProcessToolCallResult {
260 pub content: String,
261 pub is_error: bool,
262 #[serde(default)]
263 pub data: serde_json::Value,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
269#[serde(rename_all = "camelCase")]
270pub struct ProcessInferenceEventNotification {
271 pub stream_id: String,
272 pub event: InferenceEvent,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct ProcessEventsHandleNotification {
279 pub envelope: EventEnvelope,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
286#[serde(rename_all = "camelCase")]
287pub struct ProcessExtensionOwnedEvent {
288 pub extension_id: String,
289 pub event_kind: String,
290 pub schema_version: u32,
291 pub payload: serde_json::Value,
292}
293
294pub fn validate_initialize_echo(
298 manifest: &ProcessExtensionManifest,
299 manifest_toml: &str,
300 result: &ProcessInitializeResult,
301) -> anyhow::Result<()> {
302 anyhow::ensure!(
303 result.protocol_version == PROCESS_EXTENSION_PROTOCOL_VERSION,
304 "process extension {} speaks protocol {:?} but the host requires {:?}",
305 manifest.id,
306 result.protocol_version,
307 PROCESS_EXTENSION_PROTOCOL_VERSION
308 );
309 anyhow::ensure!(
310 result.extension_id == manifest.id,
311 "process extension echoed id {:?} but the manifest declares {:?}",
312 result.extension_id,
313 manifest.id
314 );
315 anyhow::ensure!(
316 result.services == manifest.provides,
317 "process extension {} echoed services {:?} but the manifest declares {:?}",
318 manifest.id,
319 result.services,
320 manifest.provides
321 );
322 let expected = manifest_checksum(manifest_toml);
323 anyhow::ensure!(
324 result.manifest_checksum == expected,
325 "process extension {} echoed manifest checksum {:?} but the configured manifest hashes \
326 to {:?}; the child is running against a different manifest",
327 manifest.id,
328 result.manifest_checksum,
329 expected
330 );
331 Ok(())
332}
333
334pub fn validate_manifest(manifest: &ProcessExtensionManifest) -> anyhow::Result<()> {
336 anyhow::ensure!(
337 !manifest.id.trim().is_empty(),
338 "process extension manifest is missing an id"
339 );
340 anyhow::ensure!(
341 !manifest.provides.is_empty(),
342 "process extension {} declares no provided services",
343 manifest.id
344 );
345 let requirement = semver::VersionReq::parse(&manifest.api_version).map_err(|err| {
346 anyhow::anyhow!(
347 "process extension {} has invalid api_version {:?}: {err}",
348 manifest.id,
349 manifest.api_version
350 )
351 })?;
352 let supported = semver::Version::parse(crate::extension::SUPPORTED_EXTENSION_API_VERSION)?;
353 anyhow::ensure!(
354 requirement.matches(&supported),
355 "process extension {} requires extension API {:?} but the host supports {}",
356 manifest.id,
357 manifest.api_version,
358 supported
359 );
360 for service in &manifest.provides {
361 let ProcessProvidedService::ToolProvider { id, tools } = service else {
362 continue;
363 };
364 validate_tool_provider(&manifest.id, id, tools)?;
365 }
366 Ok(())
367}
368
369fn validate_tool_provider(
373 extension_id: &str,
374 provider_id: &str,
375 tools: &[ToolSpec],
376) -> anyhow::Result<()> {
377 anyhow::ensure!(
378 !tools.is_empty(),
379 "process extension {extension_id} tool provider {provider_id} declares no tools"
380 );
381 let mut names = BTreeSet::new();
382 for tool in tools {
383 anyhow::ensure!(
384 !tool.name.trim().is_empty(),
385 "process extension {extension_id} tool provider {provider_id} declares a tool with \
386 an empty name"
387 );
388 anyhow::ensure!(
389 names.insert(tool.name.as_str()),
390 "process extension {extension_id} tool provider {provider_id} declares tool {:?} \
391 more than once",
392 tool.name
393 );
394 let is_object_schema = tool
395 .parameters
396 .get("type")
397 .and_then(serde_json::Value::as_str)
398 == Some("object");
399 anyhow::ensure!(
400 is_object_schema,
401 "process extension {extension_id} tool {:?} parameters must be a JSON schema object \
402 (declare `type = \"object\"`)",
403 tool.name
404 );
405 }
406 Ok(())
407}
408
409pub fn manifest_checksum(manifest_toml: &str) -> String {
412 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
413 const PRIME: u64 = 0x0000_0100_0000_01b3;
414 let mut hash = OFFSET;
415 for byte in manifest_toml.as_bytes() {
416 hash ^= u64::from(*byte);
417 hash = hash.wrapping_mul(PRIME);
418 }
419 format!("{hash:016x}")
420}