Skip to main content

zeroclaw_runtime/rpc/
types.rs

1//! Shared request/response types for the ZeroClaw RPC + gateway API surface.
2//!
3//! **Single source of truth.** Every domain's wire types live here.
4//! The RPC dispatcher, the HTTP gateway, and the TUI client all
5//! import from this module. No ad-hoc `json!()`, no duplicated structs.
6//!
7//! ## Conventions
8//!
9//! - All structs derive `Debug, Clone, Serialize, Deserialize`.
10//! - All structs use `#[serde(rename_all = "snake_case")]`.
11//! - Optional fields use `#[serde(default, skip_serializing_if = "Option::is_none")]`.
12//! - Types that already exist elsewhere (`MemoryEntry`, `CronJob`,
13//!   `CostSummary`, `SkillFrontmatter`) are re-exported, not re-defined.
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18// ── Re-exports: types that already derive Serialize + Deserialize ────
19// Consumers can `use zeroclaw_runtime::rpc::types::*` and get everything.
20
21pub use crate::cron::{CronJob, CronJobPatch, CronRun, DeliveryConfig, Schedule};
22pub use crate::rpc::session::SessionOverrides;
23pub use crate::skills::frontmatter::SkillFrontmatter;
24pub use zeroclaw_api::memory_traits::{MemoryCategory, MemoryEntry};
25pub use zeroclaw_config::cost::types::CostSummary;
26pub use zeroclaw_config::traits::{ConfigFieldEntry, PropKind};
27
28// ── Derive helper ────────────────────────────────────────────────────
29
30macro_rules! rpc_type {
31    (
32        $(#[$meta:meta])*
33        pub struct $name:ident { $($body:tt)* }
34    ) => {
35        #[derive(Debug, Clone, Serialize, Deserialize)]
36        #[serde(rename_all = "snake_case")]
37        $(#[$meta])*
38        pub struct $name { $($body)* }
39    };
40    (
41        $(#[$meta:meta])*
42        pub enum $name:ident { $($body:tt)* }
43    ) => {
44        #[derive(Debug, Clone, Serialize, Deserialize)]
45        #[serde(rename_all = "snake_case")]
46        $(#[$meta])*
47        pub enum $name { $($body)* }
48    };
49}
50
51// ══════════════════════════════════════════════════════════════════════
52// ── Core ─────────────────────────────────────────────────────────────
53// ══════════════════════════════════════════════════════════════════════
54
55rpc_type! {
56    pub struct InitializeParams {
57        #[serde(default = "default_protocol_version")]
58        pub protocol_version: u64,
59        /// TUI ID from a previous connection (reconnection).
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        pub tui_id: Option<String>,
62        /// HMAC signature proving ownership of the claimed TUI ID.
63        #[serde(default, skip_serializing_if = "Option::is_none")]
64        pub tui_sig: Option<String>,
65        /// Shell environment from the TUI process, used to forward the user's
66        /// real env (PATH, credentials, etc.) to subprocesses spawned by the
67        /// daemon on their behalf. Omitted by older clients; defaults to empty.
68        #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
69        pub env: std::collections::HashMap<String, String>,
70    }
71}
72
73fn default_protocol_version() -> u64 {
74    1
75}
76
77rpc_type! {
78    pub struct InitializeResult {
79        pub protocol_version: u64,
80        pub server_version: String,
81        /// Assigned TUI session UID.
82        #[serde(default, skip_serializing_if = "Option::is_none")]
83        pub tui_id: Option<String>,
84        /// HMAC signature for reconnection. Pass back in next initialize.
85        #[serde(default, skip_serializing_if = "Option::is_none")]
86        pub tui_sig: Option<String>,
87        /// Supported RPC method names (e.g. "session/prompt", "memory/list").
88        #[serde(default, skip_serializing_if = "Vec::is_empty")]
89        pub capabilities: Vec<String>,
90    }
91}
92
93rpc_type! {
94    pub struct StatusResult {
95        pub server_version: String,
96        pub protocol_version: u64,
97        pub active_sessions: usize,
98        pub session_ids: Vec<String>,
99    }
100}
101
102// Health: no params, result is `Value` from `health::snapshot_json()`.
103
104// ══════════════════════════════════════════════════════════════════════
105// ── TUI ──────────────────────────────────────────────────────────────
106// ══════════════════════════════════════════════════════════════════════
107
108rpc_type! {
109    pub struct TuiListEntry {
110        pub tui_id: String,
111        /// RFC 3339 timestamp (for gateway API / web frontend).
112        pub connected_at: String,
113        /// Unix epoch seconds (for TUI client relative-time display
114        /// without requiring chrono).
115        pub connected_at_unix: i64,
116        pub peer_label: String,
117        /// Transport protocol: `"unix"` or `"wss"`.
118        pub transport: String,
119    }
120}
121
122rpc_type! {
123    pub struct TuiListResult {
124        pub tuis: Vec<TuiListEntry>,
125    }
126}
127
128// ══════════════════════════════════════════════════════════════════════
129// ── Sessions ─────────────────────────────────────────────────────────
130// ══════════════════════════════════════════════════════════════════════
131
132rpc_type! {
133    /// Shared param for methods that only need a session ID:
134    /// `session/close`, `session/cancel`, `session/messages`,
135    /// `session/state`, `session/delete`.
136    pub struct SessionIdParams {
137        pub session_id: String,
138    }
139}
140
141rpc_type! {
142    pub struct SessionNewParams {
143        pub agent_alias: String,
144        #[serde(default, skip_serializing_if = "Option::is_none")]
145        pub cwd: Option<String>,
146        #[serde(default, skip_serializing_if = "Option::is_none")]
147        pub session_id: Option<String>,
148        #[serde(default, skip_serializing_if = "Option::is_none")]
149        pub tui_id: Option<String>,
150        #[serde(default, skip_serializing_if = "Option::is_none")]
151        pub exclude_memory: Option<bool>,
152        #[serde(default, skip_serializing_if = "Option::is_none")]
153        pub chat_mode: Option<ChatMode>,
154    }
155}
156
157rpc_type! {
158    #[derive(PartialEq, Eq)]
159    pub enum ChatMode {
160        Chat,
161        Acp,
162    }
163}
164
165rpc_type! {
166    pub struct SessionNewResult {
167        pub session_id: String,
168        pub agent_alias: String,
169        pub message_count: usize,
170        pub workspace_dir: String,
171    }
172}
173
174rpc_type! {
175    pub struct SessionCloseResult {
176        pub session_id: String,
177        pub closed: bool,
178    }
179}
180
181rpc_type! {
182    pub struct SessionKillParams {
183        pub session_id: String,
184    }
185}
186
187rpc_type! {
188    pub struct SessionKillResult {
189        pub session_id: String,
190        pub killed: bool,
191    }
192}
193
194rpc_type! {
195    pub struct SessionPromptParams {
196        pub session_id: String,
197        pub prompt: String,
198        /// Inline file attachments. Processed identically to `file/attach`
199        /// entries — markers are appended to the prompt before the turn runs.
200        #[serde(default, skip_serializing_if = "Vec::is_empty")]
201        pub attachments: Vec<FileEntry>,
202    }
203}
204
205rpc_type! {
206    pub struct SessionPromptResult {
207        pub session_id: String,
208        pub stop_reason: String,
209        pub content: String,
210    }
211}
212
213rpc_type! {
214    pub struct SessionConfigureParams {
215        pub session_id: String,
216        #[serde(default)]
217        pub overrides: SessionOverrides,
218    }
219}
220
221rpc_type! {
222    pub struct SessionConfigureResult {
223        pub session_id: String,
224        pub overrides: SessionOverrides,
225    }
226}
227
228rpc_type! {
229    pub struct SessionCancelResult {
230        pub session_id: String,
231        pub cancelled: bool,
232    }
233}
234
235rpc_type! {
236    pub struct SessionGitBranchResult {
237        pub session_id: String,
238        #[serde(default, skip_serializing_if = "Option::is_none")]
239        pub branch: Option<String>,
240        #[serde(default, skip_serializing_if = "Option::is_none")]
241        pub hash: Option<String>,
242    }
243}
244
245rpc_type! {
246    pub struct SessionListParams {
247        /// Full-text search query. When present, only sessions whose message
248        /// content matches (via FTS5) are returned.
249        #[serde(default, skip_serializing_if = "Option::is_none")]
250        pub query: Option<String>,
251        #[serde(default, skip_serializing_if = "Option::is_none")]
252        pub limit: Option<usize>,
253    }
254}
255
256rpc_type! {
257    pub struct SessionListResult {
258        pub sessions: Vec<SessionEntry>,
259    }
260}
261
262rpc_type! {
263    pub struct SessionEntry {
264        pub session_id: String,
265        pub session_key: String,
266        pub created_at: String,
267        pub last_activity: String,
268        pub message_count: usize,
269        #[serde(default, skip_serializing_if = "Option::is_none")]
270        pub agent_alias: Option<String>,
271        #[serde(default, skip_serializing_if = "Option::is_none")]
272        pub channel_id: Option<String>,
273        #[serde(default, skip_serializing_if = "Option::is_none")]
274        pub name: Option<String>,
275    }
276}
277
278rpc_type! {
279    pub struct SessionMessagesResult {
280        pub session_id: String,
281        pub messages: Vec<MessageEntry>,
282        /// Total messages persisted for this session. Lets the TUI
283        /// know how many pages remain before it reaches the head.
284        #[serde(default)]
285        pub total: usize,
286        /// Index of the first message in `messages` relative to the
287        /// full persisted history. Pair with `total` to compute
288        /// "page N of M" / "load older" affordances.
289        #[serde(default)]
290        pub start: usize,
291    }
292}
293
294rpc_type! {
295    /// Params for `session/messages`. `limit` + `before_index`
296    /// page-window the load so a long session doesn't slurp every
297    /// message into client memory at once. Both default to the
298    /// legacy "load everything" behaviour for callers that pre-date
299    /// the pagination change.
300    pub struct SessionMessagesParams {
301        pub session_id: String,
302        #[serde(default)]
303        pub limit: Option<usize>,
304        #[serde(default)]
305        pub before_index: Option<usize>,
306    }
307}
308
309rpc_type! {
310    pub struct MessageEntry {
311        pub role: String,
312        pub content: String,
313    }
314}
315
316rpc_type! {
317    pub struct SessionStateResult {
318        pub session_id: String,
319        pub state: String,
320        #[serde(default, skip_serializing_if = "Option::is_none")]
321        pub turn_id: Option<String>,
322        #[serde(default, skip_serializing_if = "Option::is_none")]
323        pub turn_started_at: Option<String>,
324    }
325}
326
327rpc_type! {
328    pub struct SessionDeleteResult {
329        pub session_id: String,
330        pub deleted: bool,
331    }
332}
333
334// ══════════════════════════════════════════════════════════════════════
335// ── Memory ───────────────────────────────────────────────────────────
336// ══════════════════════════════════════════════════════════════════════
337
338rpc_type! {
339    /// Params for `memory/list`. Consolidates gateway `MemoryQuery` (list mode).
340    pub struct MemoryListParams {
341        #[serde(default, skip_serializing_if = "Option::is_none")]
342        pub category: Option<String>,
343        #[serde(default, skip_serializing_if = "Option::is_none")]
344        pub session_id: Option<String>,
345        #[serde(default, skip_serializing_if = "Option::is_none")]
346        pub agent: Option<String>,
347    }
348}
349
350rpc_type! {
351    pub struct MemoryListResult {
352        pub entries: Vec<MemoryEntry>,
353        pub count: usize,
354    }
355}
356
357rpc_type! {
358    /// Params for `memory/search`. Consolidates gateway `MemoryQuery` (search mode).
359    pub struct MemorySearchParams {
360        pub query: String,
361        #[serde(default = "default_search_limit")]
362        pub limit: usize,
363        #[serde(default, skip_serializing_if = "Option::is_none")]
364        pub session_id: Option<String>,
365        #[serde(default, skip_serializing_if = "Option::is_none")]
366        pub since: Option<String>,
367        #[serde(default, skip_serializing_if = "Option::is_none")]
368        pub until: Option<String>,
369        #[serde(default, skip_serializing_if = "Option::is_none")]
370        pub agent: Option<String>,
371    }
372}
373
374fn default_search_limit() -> usize {
375    10
376}
377
378rpc_type! {
379    pub struct MemorySearchResult {
380        pub entries: Vec<MemoryEntry>,
381        pub count: usize,
382    }
383}
384
385rpc_type! {
386    /// `memory/get` params — fetch one entry's full content by key.
387    pub struct MemoryGetParams {
388        pub key: String,
389    }
390}
391
392rpc_type! {
393    /// `memory/get` result. `entry` carries the full content
394    /// the Memory pane only renders inside the detail modal —
395    /// list rows store preview-only data.
396    pub struct MemoryGetResult {
397        pub entry: Option<MemoryEntry>,
398    }
399}
400
401rpc_type! {
402    /// Params for `memory/store`. Consolidates gateway `MemoryStoreBody`.
403    pub struct MemoryStoreParams {
404        pub key: String,
405        pub content: String,
406        #[serde(default, skip_serializing_if = "Option::is_none")]
407        pub category: Option<String>,
408        #[serde(default, skip_serializing_if = "Option::is_none")]
409        pub session_id: Option<String>,
410        #[serde(default, skip_serializing_if = "Option::is_none")]
411        pub agent: Option<String>,
412    }
413}
414
415rpc_type! {
416    pub struct MemoryStoreResult {
417        pub key: String,
418        pub stored: bool,
419    }
420}
421
422rpc_type! {
423    /// Params for `memory/delete`. Consolidates gateway `MemoryDeleteQuery`.
424    pub struct MemoryDeleteParams {
425        pub key: String,
426        #[serde(default, skip_serializing_if = "Option::is_none")]
427        pub agent: Option<String>,
428    }
429}
430
431rpc_type! {
432    pub struct MemoryDeleteResult {
433        pub key: String,
434        pub deleted: bool,
435    }
436}
437
438// ══════════════════════════════════════════════════════════════════════
439// ── Cron ─────────────────────────────────────────────────────────────
440// ══════════════════════════════════════════════════════════════════════
441
442rpc_type! {
443    pub struct CronListResult {
444        pub jobs: Vec<CronJob>,
445    }
446}
447
448rpc_type! {
449    pub struct CronIdParams {
450        pub id: String,
451    }
452}
453
454rpc_type! {
455    /// Params for `cron/add`. Consolidates gateway `CronAddBody`.
456    pub struct CronAddParams {
457        pub agent: String,
458        pub schedule: String,
459        #[serde(default, skip_serializing_if = "Option::is_none")]
460        pub tz: Option<String>,
461        #[serde(default, skip_serializing_if = "Option::is_none")]
462        pub command: Option<String>,
463        #[serde(default, skip_serializing_if = "Option::is_none")]
464        pub prompt: Option<String>,
465        #[serde(default, skip_serializing_if = "Option::is_none")]
466        pub name: Option<String>,
467        #[serde(default, skip_serializing_if = "Option::is_none")]
468        pub job_type: Option<String>,
469        #[serde(default, skip_serializing_if = "Option::is_none")]
470        pub delivery: Option<DeliveryConfig>,
471        #[serde(default, skip_serializing_if = "Option::is_none")]
472        pub session_target: Option<String>,
473        #[serde(default, skip_serializing_if = "Option::is_none")]
474        pub model: Option<String>,
475        #[serde(default, skip_serializing_if = "Option::is_none")]
476        pub allowed_tools: Option<Vec<String>>,
477        #[serde(default, skip_serializing_if = "Option::is_none")]
478        pub delete_after_run: Option<bool>,
479    }
480}
481
482rpc_type! {
483    /// Params for `cron/patch`. Consolidates gateway `CronPatchBody`.
484    pub struct CronPatchParams {
485        pub id: String,
486        pub agent: String,
487        #[serde(default, skip_serializing_if = "Option::is_none")]
488        pub name: Option<String>,
489        #[serde(default, skip_serializing_if = "Option::is_none")]
490        pub schedule: Option<String>,
491        #[serde(default, skip_serializing_if = "Option::is_none")]
492        pub tz: Option<String>,
493        #[serde(default, skip_serializing_if = "Option::is_none")]
494        pub clear_tz: Option<bool>,
495        #[serde(default, skip_serializing_if = "Option::is_none")]
496        pub command: Option<String>,
497        #[serde(default, skip_serializing_if = "Option::is_none")]
498        pub prompt: Option<String>,
499    }
500}
501
502rpc_type! {
503    pub struct CronDeleteResult {
504        pub id: String,
505        pub deleted: bool,
506    }
507}
508
509rpc_type! {
510    pub struct CronRunsParams {
511        pub id: String,
512        #[serde(default, skip_serializing_if = "Option::is_none")]
513        pub limit: Option<u32>,
514    }
515}
516
517rpc_type! {
518    pub struct CronRunsResult {
519        pub runs: Vec<CronRun>,
520    }
521}
522
523rpc_type! {
524    pub struct CronTriggerResult {
525        pub id: String,
526        pub success: bool,
527        pub output: String,
528    }
529}
530
531// ══════════════════════════════════════════════════════════════════════
532// ── Config ───────────────────────────────────────────────────────────
533// ══════════════════════════════════════════════════════════════════════
534
535rpc_type! {
536    pub struct ConfigGetParams {
537        #[serde(default, skip_serializing_if = "Option::is_none")]
538        pub prop: Option<String>,
539    }
540}
541
542rpc_type! {
543    /// Returned when `config/get` is called with a specific `prop`.
544    pub struct ConfigGetPropResult {
545        pub prop: String,
546        pub value: String,
547    }
548}
549
550// Full config read returns `Value` (masked) — inherently untyped.
551
552rpc_type! {
553    /// Value is polymorphic: a JSON string passes through as-is (backward
554    /// compat); any other JSON type is coerced via `coerce_for_set_prop`.
555    pub struct ConfigSetParams {
556        pub prop: String,
557        pub value: Value,
558    }
559}
560
561rpc_type! {
562    pub struct ConfigSetResult {
563        pub prop: String,
564        pub set: bool,
565    }
566}
567
568rpc_type! {
569    pub struct ConfigValidateResult {
570        pub valid: bool,
571        #[serde(default, skip_serializing_if = "Option::is_none")]
572        pub error: Option<String>,
573    }
574}
575
576rpc_type! {
577    pub struct ConfigReloadResult {
578        pub reloading: bool,
579    }
580}
581
582rpc_type! {
583    pub struct ConfigListParams {
584        #[serde(default, skip_serializing_if = "Option::is_none")]
585        pub prefix: Option<String>,
586    }
587}
588
589rpc_type! {
590    pub struct ConfigListResult {
591        pub entries: Vec<ConfigFieldEntry>,
592    }
593}
594
595rpc_type! {
596    pub struct ConfigDeleteParams {
597        pub prop: String,
598    }
599}
600
601rpc_type! {
602    pub struct ConfigDeleteResult {
603        pub prop: String,
604        pub deleted: bool,
605    }
606}
607
608rpc_type! {
609    pub struct ConfigMapKeysParams {
610        pub path: String,
611    }
612}
613
614rpc_type! {
615    pub struct ConfigMapKeysResult {
616        pub path: String,
617        pub keys: Vec<String>,
618    }
619}
620
621rpc_type! {
622    pub struct ConfigMapKeyCreateParams {
623        pub path: String,
624        pub key: String,
625    }
626}
627
628rpc_type! {
629    pub struct ConfigMapKeyCreateResult {
630        pub path: String,
631        pub key: String,
632        pub created: bool,
633    }
634}
635
636rpc_type! {
637    pub struct ConfigMapKeyDeleteParams {
638        pub path: String,
639        pub key: String,
640    }
641}
642
643rpc_type! {
644    pub struct ConfigMapKeyDeleteResult {
645        pub path: String,
646        pub key: String,
647        pub deleted: bool,
648    }
649}
650
651rpc_type! {
652    pub struct ConfigMapKeyRenameParams {
653        pub path: String,
654        pub from: String,
655        pub to: String,
656    }
657}
658
659rpc_type! {
660    pub struct ConfigMapKeyRenameResult {
661        pub path: String,
662        pub from: String,
663        pub to: String,
664        pub renamed: bool,
665    }
666}
667
668rpc_type! {
669    /// Owned wire representation of a [`zeroclaw_config::traits::MapKeySection`].
670    /// The upstream type uses `&'static str` fields that can't round-trip
671    /// through `Deserialize`, so this owned copy serves as the wire format.
672    pub struct ConfigTemplateEntry {
673        pub path: String,
674        pub kind: zeroclaw_config::traits::MapKeyKind,
675        pub value_type: String,
676        pub description: String,
677    }
678}
679
680impl From<zeroclaw_config::traits::MapKeySection> for ConfigTemplateEntry {
681    fn from(s: zeroclaw_config::traits::MapKeySection) -> Self {
682        Self {
683            path: s.path.to_string(),
684            kind: s.kind,
685            value_type: s.value_type.to_string(),
686            description: s.description.to_string(),
687        }
688    }
689}
690
691rpc_type! {
692    pub struct ConfigTemplatesResult {
693        pub templates: Vec<ConfigTemplateEntry>,
694    }
695}
696
697// ══════════════════════════════════════════════════════════════════════
698// ── Agents ───────────────────────────────────────────────────────────
699// ══════════════════════════════════════════════════════════════════════
700
701rpc_type! {
702    pub struct AgentEntry {
703        pub alias: String,
704        pub enabled: bool,
705        pub channels: Vec<String>,
706    }
707}
708
709rpc_type! {
710    pub struct AgentsListResult {
711        pub agents: Vec<AgentEntry>,
712    }
713}
714
715rpc_type! {
716    pub struct AgentStatusEntry {
717        pub alias: String,
718        pub enabled: bool,
719        #[serde(default)]
720        pub live_sessions: usize,
721        #[serde(default)]
722        pub persisted_sessions: usize,
723        #[serde(default)]
724        pub channels: Vec<String>,
725    }
726}
727
728rpc_type! {
729    pub struct AgentsStatusResult {
730        pub agents: Vec<AgentStatusEntry>,
731    }
732}
733
734// ══════════════════════════════════════════════════════════════════════
735// ── Cost ─────────────────────────────────────────────────────────────
736// ══════════════════════════════════════════════════════════════════════
737
738rpc_type! {
739    /// Params for `cost/query`. Consolidates gateway `CostQuery`.
740    pub struct CostQueryParams {
741        #[serde(default, skip_serializing_if = "Option::is_none")]
742        pub agent: Option<String>,
743        #[serde(default, skip_serializing_if = "Option::is_none")]
744        pub from: Option<String>,
745        #[serde(default, skip_serializing_if = "Option::is_none")]
746        pub to: Option<String>,
747    }
748}
749
750// Result is `CostSummary` directly (already Serialize).
751
752// ══════════════════════════════════════════════════════════════════════
753// ── Skills ───────────────────────────────────────────────────────────
754// ══════════════════════════════════════════════════════════════════════
755
756rpc_type! {
757    /// Wire representation of a skill bundle. Consolidates gateway `BundleEntry`.
758    pub struct SkillBundleEntry {
759        pub alias: String,
760        pub directory: String,
761        pub include: Vec<String>,
762        pub exclude: Vec<String>,
763    }
764}
765
766rpc_type! {
767    pub struct SkillsBundlesResult {
768        pub bundles: Vec<SkillBundleEntry>,
769    }
770}
771
772rpc_type! {
773    pub struct SkillsListParams {
774        #[serde(default, skip_serializing_if = "Option::is_none")]
775        pub bundle: Option<String>,
776    }
777}
778
779rpc_type! {
780    /// Wire representation of a skill in a list. Consolidates gateway `SkillEntry`.
781    pub struct SkillListEntry {
782        pub bundle: String,
783        pub name: String,
784        pub directory: String,
785        pub frontmatter: SkillFrontmatter,
786    }
787}
788
789rpc_type! {
790    pub struct SkillsListResult {
791        pub skills: Vec<SkillListEntry>,
792    }
793}
794
795rpc_type! {
796    pub struct SkillsReadParams {
797        pub bundle: String,
798        pub name: String,
799    }
800}
801
802rpc_type! {
803    /// Consolidates gateway `SkillReadResponse`.
804    pub struct SkillsReadResult {
805        pub bundle: String,
806        pub name: String,
807        pub frontmatter: SkillFrontmatter,
808        pub body: String,
809    }
810}
811
812rpc_type! {
813    pub struct SkillsWriteParams {
814        pub bundle: String,
815        pub name: String,
816        pub frontmatter: SkillFrontmatter,
817        #[serde(default)]
818        pub body: String,
819    }
820}
821
822rpc_type! {
823    pub struct SkillsWriteResult {
824        pub bundle: String,
825        pub name: String,
826        pub written: bool,
827    }
828}
829
830rpc_type! {
831    pub struct SkillsDeleteParams {
832        pub bundle: String,
833        pub name: String,
834    }
835}
836
837rpc_type! {
838    pub struct SkillsDeleteResult {
839        pub bundle: String,
840        pub name: String,
841        pub deleted: bool,
842    }
843}
844
845// ══════════════════════════════════════════════════════════════════════
846// ── Personality ──────────────────────────────────────────────────────
847// ══════════════════════════════════════════════════════════════════════
848
849rpc_type! {
850    pub struct PersonalityListParams {
851        #[serde(default, skip_serializing_if = "Option::is_none")]
852        pub agent: Option<String>,
853    }
854}
855
856rpc_type! {
857    /// Consolidates gateway `PersonalityIndexEntry`.
858    pub struct PersonalityFileEntry {
859        pub filename: String,
860        pub exists: bool,
861        #[serde(default)]
862        pub size: u64,
863        #[serde(default, skip_serializing_if = "Option::is_none")]
864        pub mtime_ms: Option<i64>,
865    }
866}
867
868rpc_type! {
869    /// Consolidates gateway `PersonalityIndex`.
870    pub struct PersonalityListResult {
871        pub files: Vec<PersonalityFileEntry>,
872        pub max_chars: usize,
873    }
874}
875
876rpc_type! {
877    pub struct PersonalityGetParams {
878        pub agent: String,
879        pub filename: String,
880    }
881}
882
883rpc_type! {
884    /// Consolidates gateway `PersonalityFileResponse`.
885    pub struct PersonalityGetResult {
886        pub filename: String,
887        #[serde(default)]
888        pub content: Option<String>,
889        pub exists: bool,
890        #[serde(default)]
891        pub truncated: bool,
892        #[serde(default, skip_serializing_if = "Option::is_none")]
893        pub mtime_ms: Option<i64>,
894    }
895}
896
897rpc_type! {
898    pub struct PersonalityPutParams {
899        pub agent: String,
900        pub filename: String,
901        pub content: String,
902    }
903}
904
905rpc_type! {
906    /// Consolidates gateway `PersonalityPutResponse`.
907    pub struct PersonalityPutResult {
908        pub bytes_written: u64,
909        #[serde(default, skip_serializing_if = "Option::is_none")]
910        pub mtime_ms: Option<i64>,
911    }
912}
913
914rpc_type! {
915    pub struct PersonalityTemplatesParams {
916        #[serde(default, skip_serializing_if = "Option::is_none")]
917        pub agent: Option<String>,
918    }
919}
920
921rpc_type! {
922    /// Consolidates gateway `TemplateFile`.
923    pub struct TemplateFileEntry {
924        pub filename: String,
925        pub content: String,
926    }
927}
928
929rpc_type! {
930    /// Consolidates gateway `TemplateResponse`.
931    pub struct PersonalityTemplatesResult {
932        pub preset: String,
933        pub files: Vec<TemplateFileEntry>,
934    }
935}
936
937// ══════════════════════════════════════════════════════════════════════
938// ── Config introspection (sections, catalog, status) ─────────────────
939// ══════════════════════════════════════════════════════════════════════
940
941rpc_type! {
942    /// Consolidates gateway `CatalogModelProvider`.
943    pub struct CatalogModelProvider {
944        pub name: String,
945        pub display_name: String,
946        pub local: bool,
947    }
948}
949
950rpc_type! {
951    /// Consolidates gateway `CatalogResponse`.
952    pub struct CatalogResponse {
953        pub model_providers: Vec<CatalogModelProvider>,
954    }
955}
956
957rpc_type! {
958    pub struct CatalogModelsParams {
959        /// Accepts `model_provider` or aliased `provider` (gateway compat).
960        #[serde(alias = "provider")]
961        pub model_provider: String,
962    }
963}
964
965rpc_type! {
966    /// Consolidates gateway `ModelsResponse`.
967    pub struct CatalogModelsResult {
968        pub model_provider: String,
969        pub models: Vec<String>,
970        /// Optional pricing data keyed by model id. Populated when the
971        /// provider's `/models` endpoint returns pricing (Kilo Gateway,
972        /// OpenRouter, etc.). Absent for catalog fallbacks without pricing.
973        #[serde(default, skip_serializing_if = "Option::is_none")]
974        pub pricing: Option<std::collections::HashMap<String, zeroclaw_api::model_provider::ModelPricing>>,
975        pub local: bool,
976        pub live: bool,
977    }
978}
979
980rpc_type! {
981    /// A config section entry for the dashboard sidebar / TUI section list.
982    pub struct ConfigSectionEntry {
983        pub key: String,
984        pub label: String,
985        pub help: String,
986        pub has_picker: bool,
987        pub completed: bool,
988        /// Whether the section currently has enough usable config for the
989        /// first-run path.
990        #[serde(default)]
991        pub ready: bool,
992        /// Display group for the dashboard sidebar.
993        #[serde(default)]
994        pub group: String,
995        /// `true` when this section is part of the canonical Quickstart list.
996        #[serde(default)]
997        pub is_quickstart: bool,
998        /// Editor shape (direct form / one-tier alias map / typed-family map /
999        /// backend picker).
1000        #[serde(default, skip_serializing_if = "Option::is_none")]
1001        pub shape: Option<zeroclaw_config::sections::SectionShape>,
1002    }
1003}
1004
1005rpc_type! {
1006    /// Response for `config/sections`.
1007    pub struct ConfigSectionsResult {
1008        pub sections: Vec<ConfigSectionEntry>,
1009    }
1010}
1011
1012rpc_type! {
1013    /// Config readiness status for the dashboard/TUI.
1014    pub struct ConfigStatusResult {
1015        pub needs_quickstart: bool,
1016        pub reason: String,
1017        pub has_partial_state: bool,
1018        pub missing: Vec<String>,
1019    }
1020}
1021
1022rpc_type! {
1023    /// Consolidates gateway `PickerItem`.
1024    pub struct PickerItem {
1025        pub key: String,
1026        pub label: String,
1027        #[serde(default, skip_serializing_if = "Option::is_none")]
1028        pub description: Option<String>,
1029        #[serde(default, skip_serializing_if = "Option::is_none")]
1030        pub badge: Option<String>,
1031    }
1032}
1033
1034rpc_type! {
1035    /// Consolidates gateway `PickerResponse`.
1036    pub struct PickerResponse {
1037        pub section: String,
1038        pub items: Vec<PickerItem>,
1039        pub help: String,
1040    }
1041}
1042
1043rpc_type! {
1044    pub struct SectionSelectParams {
1045        pub section: String,
1046        pub key: String,
1047        #[serde(default, skip_serializing_if = "Option::is_none")]
1048        pub alias: Option<String>,
1049    }
1050}
1051
1052rpc_type! {
1053    /// Consolidates gateway `SelectItemResponse`.
1054    pub struct SelectItemResponse {
1055        pub fields_prefix: String,
1056        pub created: bool,
1057    }
1058}
1059
1060// ══════════════════════════════════════════════════════════════════════
1061// ── File attachments ─────────────────────────────────────────────────
1062// ══════════════════════════════════════════════════════════════════════
1063
1064#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1065#[serde(rename_all = "snake_case")]
1066/// Source hint for how the client obtained the file.
1067pub enum FileSource {
1068    Clipboard,
1069    #[default]
1070    File,
1071}
1072
1073rpc_type! {
1074    /// A single file entry in a `file/attach` request. Either `path` (daemon
1075    /// reads from local disk — Unix socket only) or `data_b64` (client sends
1076    /// base64-encoded bytes) must be present.
1077    pub struct FileEntry {
1078        #[serde(default, skip_serializing_if = "Option::is_none")]
1079        pub path: Option<String>,
1080        #[serde(default, skip_serializing_if = "Option::is_none")]
1081        pub data_b64: Option<String>,
1082        #[serde(default, skip_serializing_if = "Option::is_none")]
1083        pub filename: Option<String>,
1084        #[serde(default, skip_serializing_if = "Option::is_none")]
1085        pub mime_type: Option<String>,
1086        #[serde(default)]
1087        pub source: FileSource,
1088    }
1089}
1090
1091rpc_type! {
1092    pub struct FileAttachParams {
1093        pub session_id: String,
1094        pub files: Vec<FileEntry>,
1095    }
1096}
1097
1098rpc_type! {
1099    /// Result for a single file in a `file/attach` response.
1100    pub struct FileEntryResult {
1101        pub ref_id: String,
1102        pub marker: String,
1103        pub workspace_path: String,
1104        pub size_bytes: u64,
1105        pub deduplicated: bool,
1106    }
1107}
1108
1109rpc_type! {
1110    pub struct FileAttachResult {
1111        pub files: Vec<FileEntryResult>,
1112    }
1113}
1114
1115// ══════════════════════════════════════════════════════════════════════
1116// ── Session approval ─────────────────────────────────────────────────
1117// ══════════════════════════════════════════════════════════════════════
1118
1119rpc_type! {
1120    pub struct SessionApproveParams {
1121        pub session_id: String,
1122        pub request_id: String,
1123        pub decision: String,
1124        #[serde(default, skip_serializing_if = "Option::is_none")]
1125        pub replacement: Option<String>,
1126    }
1127}
1128
1129rpc_type! {
1130    pub struct SessionApproveResult {
1131        pub session_id: String,
1132        pub request_id: String,
1133        pub acknowledged: bool,
1134    }
1135}
1136
1137// ══════════════════════════════════════════════════════════════════════
1138// ── Logs ─────────────────────────────────────────────────────────────
1139// ══════════════════════════════════════════════════════════════════════
1140
1141rpc_type! {
1142    pub struct LogsSubscribeResult {
1143        pub subscribed: bool,
1144    }
1145}
1146
1147rpc_type! {
1148    pub struct LogsQueryParams {
1149        #[serde(default)]
1150        pub since_ts: Option<String>,
1151        #[serde(default)]
1152        pub until_ts: Option<String>,
1153        #[serde(default)]
1154        pub until_id: Option<String>,
1155        #[serde(default)]
1156        pub severity_min: Option<u8>,
1157        #[serde(default)]
1158        pub q: Option<String>,
1159        #[serde(default)]
1160        pub category: Option<String>,
1161        #[serde(default)]
1162        pub action: Option<String>,
1163        #[serde(default)]
1164        pub outcome: Option<String>,
1165        #[serde(default)]
1166        pub trace_id: Option<String>,
1167        #[serde(default)]
1168        pub hide_internal: bool,
1169        #[serde(default)]
1170        pub limit: Option<usize>,
1171    }
1172}
1173
1174rpc_type! {
1175    pub struct LogsQueryResult {
1176        pub events: Vec<serde_json::Value>,
1177        pub next_cursor: Option<(String, String)>,
1178        pub at_end: bool,
1179    }
1180}
1181
1182rpc_type! {
1183    /// `logs/get` params — fetch a single event by id.
1184    pub struct LogsGetParams {
1185        pub id: String,
1186    }
1187}
1188
1189rpc_type! {
1190    /// `logs/get` result. `event` is the full `LogEvent` payload
1191    /// (attributes, attribution map, span ids, …) that the Logs pane
1192    /// only renders inside the detail modal — list rows store
1193    /// preview-only data.
1194    pub struct LogsGetResult {
1195        pub event: serde_json::Value,
1196    }
1197}
1198
1199// ══════════════════════════════════════════════════════════════════════
1200// ── Session update notifications ─────────────────────────────────────
1201// ══════════════════════════════════════════════════════════════════════
1202
1203/// Typed session update events pushed via `session/update` notifications.
1204/// Replaces the hand-built `notification_for_turn_event` function.
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1206#[serde(tag = "type", rename_all = "snake_case")]
1207pub enum SessionUpdateEvent {
1208    AgentMessageChunk {
1209        session_id: String,
1210        text: String,
1211    },
1212    AgentThoughtChunk {
1213        session_id: String,
1214        text: String,
1215    },
1216    ToolCall {
1217        session_id: String,
1218        tool_call_id: String,
1219        name: String,
1220        raw_input: Value,
1221    },
1222    ToolResult {
1223        session_id: String,
1224        tool_call_id: String,
1225        name: String,
1226        raw_output: String,
1227    },
1228    ApprovalRequest {
1229        session_id: String,
1230        request_id: String,
1231        tool_name: String,
1232        arguments_summary: String,
1233        timeout_secs: u64,
1234    },
1235    /// Per-LLM-call token usage. `input_tokens` is the cumulative context size
1236    /// for this turn; `max_context_tokens` is the configured limit. Both may be
1237    /// absent when the provider doesn't report usage.
1238    ContextUsage {
1239        session_id: String,
1240        #[serde(default, skip_serializing_if = "Option::is_none")]
1241        input_tokens: Option<u64>,
1242        #[serde(default, skip_serializing_if = "Option::is_none")]
1243        max_context_tokens: Option<u64>,
1244    },
1245    /// Terminal event for a turn. Replaces the response of `session/prompt`.
1246    /// `outcome` distinguishes a clean finish from a user-initiated cancel.
1247    TurnComplete {
1248        session_id: String,
1249        outcome: TurnCompletionOutcome,
1250        /// Final assistant text (Completed) or partial accumulated text
1251        /// at cancel point (Cancelled).
1252        content: String,
1253    },
1254}
1255
1256/// Wire-stable subset of [`crate::rpc::turn::TurnOutcome`] for
1257/// `TurnComplete`. `messages` is intentionally not on the wire — the TUI
1258/// rebuilds from streamed chunks.
1259#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1260#[serde(rename_all = "snake_case")]
1261pub enum TurnCompletionOutcome {
1262    Completed,
1263    Cancelled,
1264    Failed,
1265}
1266
1267// ══════════════════════════════════════════════════════════════════════
1268// ── Quickstart ───────────────────────────────────────────────────────
1269// ══════════════════════════════════════════════════════════════════════
1270//
1271// RPC mirror of the HTTP `/api/quickstart/*` routes in
1272// `zeroclaw-gateway`. The wire shapes are deliberately identical so the
1273// drift test in `tests/quickstart_drift.rs` can submit the same fixture
1274// `BuilderSubmission` through both transports and assert identical
1275// on-disk delta + identical response shape.
1276
1277pub use crate::quickstart::{
1278    AppliedAgent, FieldDescriptor, FieldSection, QuickstartError, QuickstartStep, Surface,
1279};
1280pub use zeroclaw_config::presets::BuilderSubmission;
1281
1282rpc_type! {
1283    /// Mirrors `zeroclaw_gateway::api_quickstart::QuickstartState`.
1284    pub struct QuickstartStateResult {
1285        pub quickstart_completed: bool,
1286        pub agents: Vec<String>,
1287        pub risk_profiles: Vec<String>,
1288        pub runtime_profiles: Vec<String>,
1289        /// `<provider_type>.<alias>` refs.
1290        pub model_providers: Vec<String>,
1291        /// `<channel_type>.<alias>` refs.
1292        pub channels: Vec<String>,
1293        /// Subset of `channels` not yet bound to any agent — safe to
1294        /// reuse without breaking the one-channel-one-agent invariant.
1295        #[serde(default)]
1296        pub unassigned_channels: Vec<String>,
1297        /// `<storage_type>.<alias>` refs.
1298        pub storage: Vec<String>,
1299        /// Picker rows for "Create new model provider" — sourced from
1300        /// the canonical `zeroclaw_providers::list_model_providers()`
1301        /// registry by [`crate::quickstart::snapshot_state`].
1302        pub model_provider_types: Vec<QuickstartTypeOption>,
1303        /// Picker rows for "Create new channel" — sourced from the
1304        /// schema's `ChannelsConfig` by walking its serialised
1305        /// top-level keys, so adding a channel family in the schema
1306        /// surfaces here automatically.
1307        pub channel_types: Vec<QuickstartTypeOption>,
1308    }
1309}
1310
1311rpc_type! {
1312    /// One row in the Quickstart "Create new …" picker. The TUI and
1313    /// web surfaces both render this list as-is — no hardcoded
1314    /// option lists on either side.
1315    pub struct QuickstartTypeOption {
1316        /// Canonical kebab-case identifier written into config
1317        /// (`anthropic`, `telegram`, `wecom-ws`, …).
1318        pub kind: String,
1319        /// Human-readable picker label.
1320        pub display_name: String,
1321        /// `true` when the entry runs locally and needs no remote
1322        /// credential. Always `false` for channels.
1323        pub local: bool,
1324    }
1325}
1326
1327rpc_type! {
1328    pub struct QuickstartValidateParams {
1329        pub submission: BuilderSubmission,
1330    }
1331}
1332
1333rpc_type! {
1334    pub struct QuickstartFieldsParams {
1335        pub section: FieldSection,
1336        pub type_key: String,
1337    }
1338}
1339
1340rpc_type! {
1341    pub struct QuickstartFieldsResult {
1342        pub fields: Vec<FieldDescriptor>,
1343    }
1344}
1345
1346/// Tagged enum — matches the HTTP route's `ValidateResult` shape so
1347/// the drift test can compare bytes.
1348#[derive(Debug, Clone, Serialize, Deserialize)]
1349#[serde(tag = "kind", rename_all = "snake_case")]
1350pub enum QuickstartValidateResult {
1351    Ok,
1352    Errors { errors: Vec<QuickstartError> },
1353}
1354
1355rpc_type! {
1356    pub struct QuickstartApplyParams {
1357        pub submission: BuilderSubmission,
1358    }
1359}
1360
1361/// Tagged enum — matches the HTTP route's `ApplyResult` shape.
1362#[derive(Debug, Clone, Serialize, Deserialize)]
1363#[serde(tag = "kind", rename_all = "snake_case")]
1364pub enum QuickstartApplyResult {
1365    Applied {
1366        agent: AppliedAgent,
1367        /// `true` when the in-place daemon reload was signalled.
1368        /// `false` when no reload tx was attached (e.g. test harness)
1369        /// — caller must restart the daemon manually to pick up the
1370        /// change.
1371        daemon_restarted: bool,
1372    },
1373    Errors {
1374        errors: Vec<QuickstartError>,
1375    },
1376}
1377
1378rpc_type! {
1379    pub struct QuickstartDismissParams {
1380        pub run_id: String,
1381        /// Surface that emitted the dismissal. Deserialised straight
1382        /// into the typed enum — no string-match at the boundary.
1383        pub surface: Surface,
1384        #[serde(default)]
1385        pub last_step: Option<QuickstartStep>,
1386    }
1387}
1388
1389rpc_type! {
1390    pub struct QuickstartDismissResult {
1391        pub recorded: bool,
1392    }
1393}