Skip to main content

roder_api/process_extension/
dispatch.rs

1//! Subagent-dispatcher and task-executor payloads for the process-extension
2//! protocol (roadmap phase 95).
3//!
4//! These mirror the canonical [`crate::subagents`] and [`crate::tasks`]
5//! contracts so any-language children can dispatch long-running work (for
6//! example remote Cursor cloud agents) through ordinary Roder surfaces. The
7//! flow matches `inference/streamTurn`: the host sends a request carrying a
8//! host-chosen id, the child acks it, then streams `subagents/event` /
9//! `tasks/event` notifications until a terminal `completed` / `failed` /
10//! `cancelled` event.
11
12use serde::{Deserialize, Serialize};
13
14use crate::events::{ThreadId, TurnId};
15use crate::subagents::{SubagentDefinition, SubagentRequest, SubagentResult};
16use crate::tasks::{TaskExecutionResult, TaskOutputStream, TaskSpec};
17
18/// `subagents/definitions` params (host -> child).
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "camelCase")]
21pub struct ProcessSubagentDefinitionsParams {
22    pub dispatcher_id: String,
23}
24
25/// `subagents/definitions` result (child -> host).
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct ProcessSubagentDefinitionsResult {
29    pub definitions: Vec<SubagentDefinition>,
30}
31
32/// `subagents/dispatch` params: a canonical request plus parent provenance
33/// and a host-chosen dispatch id the child must echo and stream against.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35#[serde(rename_all = "camelCase")]
36pub struct ProcessSubagentDispatchParams {
37    pub dispatcher_id: String,
38    pub dispatch_id: String,
39    pub parent_thread_id: ThreadId,
40    pub parent_turn_id: TurnId,
41    pub request: SubagentRequest,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "camelCase")]
46pub struct ProcessSubagentDispatchAck {
47    pub dispatch_id: String,
48}
49
50/// `subagents/event` notification payload (child -> host). The host routes
51/// by `dispatch_id`; `completed`/`failed` are terminal.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53#[serde(rename_all = "camelCase")]
54pub struct ProcessSubagentEventNotification {
55    pub dispatch_id: String,
56    pub event: ProcessSubagentEvent,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60#[serde(rename_all = "snake_case", tag = "type")]
61pub enum ProcessSubagentEvent {
62    /// Non-terminal progress (e.g. a remote agent lifecycle transition).
63    /// Payloads must already be redacted by the child.
64    Status {
65        status: String,
66        #[serde(default, skip_serializing_if = "Option::is_none")]
67        detail: Option<String>,
68    },
69    Completed {
70        result: Box<SubagentResult>,
71    },
72    Failed {
73        error: String,
74    },
75}
76
77/// `subagents/cancel` params (host -> child request; result is empty).
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "camelCase")]
80pub struct ProcessSubagentCancelParams {
81    pub dispatcher_id: String,
82    pub dispatch_id: String,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub reason: Option<String>,
85}
86
87/// `tasks/spec` params (host -> child).
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "camelCase")]
90pub struct ProcessTaskSpecParams {
91    pub executor_id: String,
92}
93
94/// `tasks/spec` result (child -> host).
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96#[serde(rename_all = "camelCase")]
97pub struct ProcessTaskSpecResult {
98    pub spec: TaskSpec,
99}
100
101/// `tasks/execute` params: canonical task input plus execution provenance
102/// and a host-chosen execution id the child must echo and stream against.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104#[serde(rename_all = "camelCase")]
105pub struct ProcessTaskExecuteParams {
106    pub executor_id: String,
107    pub execution_id: String,
108    pub task_id: String,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub thread_id: Option<ThreadId>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub turn_id: Option<TurnId>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub workspace_root: Option<String>,
115    pub input: serde_json::Value,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "camelCase")]
120pub struct ProcessTaskExecuteAck {
121    pub execution_id: String,
122}
123
124/// `tasks/event` notification payload (child -> host). The host routes by
125/// `execution_id`; `completed`/`failed` are terminal.
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct ProcessTaskEventNotification {
129    pub execution_id: String,
130    pub event: ProcessTaskEvent,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134#[serde(rename_all = "snake_case", tag = "type")]
135pub enum ProcessTaskEvent {
136    /// Incremental output forwarded into the task's output sink.
137    Output {
138        stream: TaskOutputStream,
139        chunk: String,
140    },
141    Completed {
142        result: TaskExecutionResult,
143    },
144    Failed {
145        error: String,
146    },
147}
148
149/// `tasks/cancel` params (host -> child request; result is empty).
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151#[serde(rename_all = "camelCase")]
152pub struct ProcessTaskCancelParams {
153    pub executor_id: String,
154    pub execution_id: String,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub reason: Option<String>,
157}