1use crate::{ControlVersion, ConversationSurface, RunStatus};
4use serde::{Deserialize, Serialize};
5
6pub const VIEW_GET: &str = "/pod/talk/view/get";
7pub const TURN_SUBMIT: &str = "/pod/talk/turn/submit";
8pub const REQUEST_GET: &str = "/pod/talk/request/get";
9pub const REQUEST_CANCEL: &str = "/pod/talk/request/cancel";
10pub const REPLY_READ: &str = "/pod/talk/reply/read";
11pub const INTERACTION_ANSWER: &str = "/pod/talk/interaction/answer";
12pub const AUDIO_PREPARE: &str = "/pod/talk/audio/upload/prepare";
13pub const AUDIO_TRANSCRIBE: &str = "/pod/talk/audio/transcribe";
14pub const SPEECH_PREPARE: &str = "/pod/talk/speech/prepare";
15pub const MEDIA_RESOLVE: &str = "/pod/talk/media/resolve";
16pub const CHANGED: &str = "/pod/talk/changed";
17pub const REQUEST_TARGETS: &[&str] = &[
18 VIEW_GET,
19 TURN_SUBMIT,
20 REQUEST_GET,
21 REQUEST_CANCEL,
22 REPLY_READ,
23 INTERACTION_ANSWER,
24 AUDIO_PREPARE,
25 AUDIO_TRANSCRIBE,
26 SPEECH_PREPARE,
27 MEDIA_RESOLVE,
28];
29pub fn is_request_target(target: &str) -> bool {
30 REQUEST_TARGETS.contains(&target)
31}
32
33pub const FRAME_BYTES: usize = 64 * 1024;
34pub const CHANGED_BYTES: usize = 2048;
35pub const TEXT_BYTES: usize = 4096;
36pub const REPLY_BYTES: usize = 16 * 1024;
37pub const PENDING_BYTES: usize = 4096;
38pub const ID_BYTES: usize = 256;
39pub const AUDIO_BYTES: u32 = 960_044;
40pub const AUDIO_DURATION_MS: u32 = 30_000;
41
42#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct Context {
45 pub owner_id: String,
46 pub scope_id: String,
47 pub surface_id: ConversationSurface,
48 pub thread_id: Option<String>,
49 pub control_version: ControlVersion,
50 pub history_version: String,
52}
53#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct Capabilities {
56 pub submit: bool,
57 pub answer: bool,
58 pub transcribe: bool,
59 pub speak: bool,
60 pub unavailable_reason: Option<String>,
61}
62#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum Phase {
65 Unknown,
66 Idle,
67 Queued,
68 Running,
69 Pending,
70 Settled,
71}
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(rename_all = "camelCase", deny_unknown_fields)]
74pub struct Pending {
75 pub thread_id: String,
76 pub request_id: String,
77 pub batch_id: String,
78 pub text: String,
79 pub action_count: u32,
80 pub complete: bool,
81 pub can_approve: bool,
83}
84#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase", deny_unknown_fields)]
86pub struct Reply {
87 pub thread_id: String,
88 pub request_id: String,
89 pub message_id: String,
90 pub content_revision: String,
92 pub offset: u32,
94 pub next_offset: Option<u32>,
95 pub previous_offset: Option<u32>,
96 pub text: String,
97 pub has_unsupported_parts: bool,
98}
99#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
100#[serde(rename_all = "camelCase", deny_unknown_fields)]
101pub struct View {
102 pub context: Context,
103 pub capabilities: Capabilities,
104 pub phase: Phase,
105 pub active_request_id: Option<String>,
106 pub queue_count: u32,
107 pub pending: Option<Pending>,
108 pub reply: Option<Reply>,
109 pub settlement: Option<Settlement>,
112}
113#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "camelCase", deny_unknown_fields)]
115pub struct Outcome {
116 pub status: RunStatus,
117 pub error_code: Option<String>,
118 pub message: Option<String>,
119}
120#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122pub struct Settlement {
123 pub request_id: String,
124 pub outcome: Outcome,
126}
127#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
128#[serde(rename_all = "camelCase", deny_unknown_fields)]
129pub struct RequestResult {
130 pub context: Context,
131 pub request_id: String,
132 pub admission: Option<crate::RequestAdmissionState>,
133 pub phase: Phase,
134 pub outcome: Option<Outcome>,
135 pub pending: Option<Pending>,
136 pub reply: Option<Reply>,
137}
138#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(rename_all = "camelCase", deny_unknown_fields)]
140pub struct Receipt {
141 pub request_id: String,
142 pub thread_id: Option<String>,
143 pub disposition: crate::TurnSubmitDisposition,
144}
145#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
146#[serde(rename_all = "camelCase", deny_unknown_fields)]
147pub struct Changed {
148 pub owner_id: String,
149 pub scope_id: String,
150 pub surface_id: ConversationSurface,
151}
152#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
153#[serde(deny_unknown_fields)]
154pub struct ViewGet {}
155#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
156#[serde(rename_all = "camelCase", deny_unknown_fields)]
157pub struct TurnSubmit {
158 pub owner_id: String,
159 pub request_id: String,
160 pub text: String,
161 pub occurred_at_unix_ms: i64,
162}
163#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
164#[serde(rename_all = "camelCase", deny_unknown_fields)]
165pub struct RequestGet {
166 pub owner_id: String,
167 pub request_id: String,
168}
169#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct RequestCancel {
172 pub owner_id: String,
173 pub request_id: String,
174 #[serde(default)]
175 pub thread_id: Option<String>,
176}
177#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase", deny_unknown_fields)]
179pub struct ReplyRead {
180 pub owner_id: String,
181 pub thread_id: String,
182 pub request_id: String,
183 pub message_id: String,
184 pub content_revision: String,
185 pub offset: u32,
186}
187#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct InteractionAnswer {
190 pub owner_id: String,
191 pub thread_id: String,
192 pub request_id: String,
193 pub batch_id: String,
194 pub operation_id: String,
195 pub text: String,
196 pub occurred_at_unix_ms: i64,
197}
198#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
199#[serde(rename_all = "snake_case")]
200pub enum AnswerDisposition {
201 Accepted,
202 Clarify,
203 Resolved,
204 Expired,
205 Unsupported,
206}
207#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct AnswerReceipt {
210 pub operation_id: String,
211 pub disposition: AnswerDisposition,
212}
213
214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
215#[serde(rename_all = "camelCase", deny_unknown_fields)]
216pub struct AudioPrepare {
217 pub owner_id: String,
218 pub operation_id: String,
219 pub size_bytes: u32,
220 pub duration_ms: u32,
221 pub sha256: String,
222}
223#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
224#[serde(rename_all = "camelCase", deny_unknown_fields)]
225pub struct AudioTranscribe {
226 pub owner_id: String,
227 pub operation_id: String,
228 pub uri: String,
229}
230#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
231#[serde(rename_all = "camelCase", deny_unknown_fields)]
232pub struct Transcript {
233 pub operation_id: String,
234 pub text: String,
235}
236#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
237#[serde(rename_all = "camelCase", deny_unknown_fields)]
238pub struct SpeechPrepare {
239 pub owner_id: String,
240 pub operation_id: String,
241 pub thread_id: String,
242 pub request_id: String,
243 pub message_id: String,
244 pub content_revision: String,
245}
246#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
247#[serde(rename_all = "camelCase", deny_unknown_fields)]
248pub struct Speech {
249 pub operation_id: String,
250 pub uri: Option<String>,
251 pub text_only_reason: Option<String>,
252}
253#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
254#[serde(rename_all = "camelCase", deny_unknown_fields)]
255pub struct MediaResolve {
256 pub owner_id: String,
257 pub uri: String,
258}
259pub type UploadGrant = artifact_api::PrepareArtifactUploadResponse;
261#[derive(Clone, Debug, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase", deny_unknown_fields)]
263pub struct ResolvedMedia {
264 pub descriptor: artifact_api::ResolveArtifactResponse,
265 pub sha256: String,
266}
267pub const SPEECH_TEXT_BYTES: usize = 4096;
268pub const SPEECH_BYTES: usize = 1_536_044;
269pub const SPEECH_DURATION_MS: u32 = 32_000;
270pub fn encoded_payload_fits(value: &impl Serialize, limit: usize) -> bool {
273 serde_json::to_string(value)
274 .ok()
275 .and_then(|payload| serde_json::to_string(&payload).ok())
276 .is_some_and(|quoted| quoted.len().saturating_add(1024) <= limit)
277}
278
279pub fn valid_identifier(value: &str) -> bool {
280 !value.trim().is_empty() && value.len() <= ID_BYTES && !value.chars().any(char::is_control)
281}
282
283pub fn valid_text(value: &str) -> bool {
284 !value.trim().is_empty() && value.len() <= TEXT_BYTES && !value.contains('\0')
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 #[test]
291 fn talk_targets_are_separate_from_chat() {
292 for target in REQUEST_TARGETS {
293 assert!(is_request_target(target));
294 assert!(!crate::is_conversation_request_target(target));
295 }
296 }
297 #[test]
298 fn changed_and_escaped_text_fit_the_wire_budget() {
299 let changed = Changed {
300 owner_id: "core:test".into(),
301 scope_id: "space".into(),
302 surface_id: ConversationSurface::client_personal("user").unwrap(),
303 };
304 assert!(encoded_payload_fits(&changed, CHANGED_BYTES));
305 assert!(!encoded_payload_fits(
306 &"\"\\\n".repeat(REPLY_BYTES),
307 FRAME_BYTES
308 ));
309 }
310 #[test]
311 fn input_limits_are_utf8_bytes_and_reject_nul() {
312 assert!(valid_text("打开灯"));
313 assert!(!valid_text(&"猫".repeat(TEXT_BYTES / 3 + 1)));
314 assert!(!valid_text("yes\0no"));
315 assert!(!valid_identifier("request\nother"));
316 }
317}