Skip to main content

kcode_k1_chat_web_code_output/
lib.rs

1#![forbid(unsafe_code)]
2
3use kcode_k1_chat_boxes::ToolCallId;
4pub use kcode_k1_web_code_ktool_protocol::{
5    Language, WEB_CODE_CHECK_METADATA_TYPE, WEB_CODE_CREATE_FILE_METADATA_TYPE,
6    WEB_CODE_CREATE_METADATA_TYPE, WEB_CODE_DELETE_FILE_METADATA_TYPE, WEB_CODE_DOCS_METADATA_TYPE,
7    WEB_CODE_OPEN_METADATA_TYPE, WEB_CODE_OVERWRITE_METADATA_TYPE, WEB_CODE_PUBLISH_METADATA_TYPE,
8    WEB_CODE_RENAME_FILE_METADATA_TYPE,
9};
10use serde::Serialize;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct WebCodeOutput {
14    pub tool_call_id: ToolCallId,
15    pub messages: Vec<String>,
16    pub message_titles: Vec<Option<String>>,
17    pub result: Result<String, String>,
18    pub metadata_type: String,
19    pub metadata_contents: String,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum PublishStatus {
24    Published,
25    Idempotent,
26    Conflict,
27}
28
29impl PublishStatus {
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::Published => "published",
33            Self::Idempotent => "idempotent",
34            Self::Conflict => "conflict",
35        }
36    }
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum Operation {
41    Create,
42    Docs,
43    Open,
44    Overwrite,
45    CreateFile,
46    RenameFile,
47    DeleteFile,
48    Check,
49    Publish,
50}
51
52impl Operation {
53    pub const fn metadata_type(self) -> &'static str {
54        match self {
55            Self::Create => WEB_CODE_CREATE_METADATA_TYPE,
56            Self::Docs => WEB_CODE_DOCS_METADATA_TYPE,
57            Self::Open => WEB_CODE_OPEN_METADATA_TYPE,
58            Self::Overwrite => WEB_CODE_OVERWRITE_METADATA_TYPE,
59            Self::CreateFile => WEB_CODE_CREATE_FILE_METADATA_TYPE,
60            Self::RenameFile => WEB_CODE_RENAME_FILE_METADATA_TYPE,
61            Self::DeleteFile => WEB_CODE_DELETE_FILE_METADATA_TYPE,
62            Self::Check => WEB_CODE_CHECK_METADATA_TYPE,
63            Self::Publish => WEB_CODE_PUBLISH_METADATA_TYPE,
64        }
65    }
66}
67
68#[derive(Serialize)]
69struct DocsMetadata<'a> {
70    status: &'static str,
71    name: &'a str,
72    version: &'a str,
73}
74
75#[derive(Serialize)]
76struct SourceMetadata<'a> {
77    status: &'static str,
78    name: &'a str,
79    version: &'a str,
80    language: &'static str,
81    file_message_start: u64,
82    file_message_count: u64,
83    paths: &'a [String],
84}
85
86#[derive(Serialize)]
87struct OverwriteMetadata<'a> {
88    status: &'static str,
89    box_id: u64,
90    path: &'a str,
91    version: &'a str,
92}
93
94#[derive(Serialize)]
95struct CreateFileMetadata<'a> {
96    status: &'static str,
97    box_id: u64,
98    path: &'a str,
99    version: &'a str,
100}
101
102#[derive(Serialize)]
103struct RenameFileMetadata<'a> {
104    status: &'static str,
105    box_id: u64,
106    path: &'a str,
107    destination: &'a str,
108    version: &'a str,
109}
110
111#[derive(Serialize)]
112struct DeleteFileMetadata<'a> {
113    status: &'static str,
114    box_id: u64,
115    path: &'a str,
116    version: &'a str,
117}
118
119#[derive(Serialize)]
120struct CheckMetadata<'a> {
121    status: &'static str,
122    fresh: bool,
123    diagnostics: Option<&'a str>,
124}
125
126#[derive(Serialize)]
127struct PublishMetadata<'a> {
128    status: &'static str,
129    version: &'a str,
130    source_object: &'a str,
131    winning: &'a str,
132    submitted: Option<&'a str>,
133    check: &'static str,
134}
135
136#[derive(Serialize)]
137struct ErrorMetadata<'a> {
138    status: &'static str,
139    code: &'a str,
140    source_object: Option<&'a str>,
141}
142
143pub fn docs(
144    tool_call_id: ToolCallId,
145    messages: Vec<String>,
146    result: Result<String, String>,
147    name: &str,
148    version: &str,
149) -> WebCodeOutput {
150    build(
151        tool_call_id,
152        messages,
153        Vec::new(),
154        result,
155        WEB_CODE_DOCS_METADATA_TYPE,
156        &DocsMetadata {
157            status: "found",
158            name,
159            version,
160        },
161    )
162}
163
164#[allow(clippy::too_many_arguments)]
165pub fn create(
166    tool_call_id: ToolCallId,
167    messages: Vec<String>,
168    result: Result<String, String>,
169    name: &str,
170    version: &str,
171    language: Language,
172    paths: &[String],
173) -> WebCodeOutput {
174    source(
175        tool_call_id,
176        messages,
177        result,
178        WEB_CODE_CREATE_METADATA_TYPE,
179        "created",
180        name,
181        version,
182        language,
183        paths,
184    )
185}
186
187#[allow(clippy::too_many_arguments)]
188pub fn open(
189    tool_call_id: ToolCallId,
190    messages: Vec<String>,
191    result: Result<String, String>,
192    name: &str,
193    version: &str,
194    language: Language,
195    paths: &[String],
196) -> WebCodeOutput {
197    source(
198        tool_call_id,
199        messages,
200        result,
201        WEB_CODE_OPEN_METADATA_TYPE,
202        "opened",
203        name,
204        version,
205        language,
206        paths,
207    )
208}
209
210#[allow(clippy::too_many_arguments)]
211fn source(
212    tool_call_id: ToolCallId,
213    messages: Vec<String>,
214    result: Result<String, String>,
215    metadata_type: &str,
216    status: &'static str,
217    name: &str,
218    version: &str,
219    language: Language,
220    paths: &[String],
221) -> WebCodeOutput {
222    assert_eq!(
223        messages.len(),
224        paths.len(),
225        "every source message must have one path"
226    );
227    let count = u64::try_from(paths.len()).expect("file count fits u64");
228    let titles = paths
229        .iter()
230        .map(|path| Some(format!("File: {path}")))
231        .collect();
232    build(
233        tool_call_id,
234        messages,
235        titles,
236        result,
237        metadata_type,
238        &SourceMetadata {
239            status,
240            name,
241            version,
242            language: language.as_str(),
243            file_message_start: 1,
244            file_message_count: count,
245            paths,
246        },
247    )
248}
249
250pub fn overwrite(
251    tool_call_id: ToolCallId,
252    result: Result<String, String>,
253    box_id: u64,
254    path: &str,
255    version: &str,
256) -> WebCodeOutput {
257    build(
258        tool_call_id,
259        Vec::new(),
260        Vec::new(),
261        result,
262        WEB_CODE_OVERWRITE_METADATA_TYPE,
263        &OverwriteMetadata {
264            status: "overwritten",
265            box_id,
266            path,
267            version,
268        },
269    )
270}
271
272pub fn create_file(
273    tool_call_id: ToolCallId,
274    contents: String,
275    result: Result<String, String>,
276    box_id: u64,
277    path: &str,
278    version: &str,
279) -> WebCodeOutput {
280    build(
281        tool_call_id,
282        vec![contents],
283        vec![Some(format!("File: {path}"))],
284        result,
285        WEB_CODE_CREATE_FILE_METADATA_TYPE,
286        &CreateFileMetadata {
287            status: "created",
288            box_id,
289            path,
290            version,
291        },
292    )
293}
294
295#[allow(clippy::too_many_arguments)]
296pub fn rename_file(
297    tool_call_id: ToolCallId,
298    contents: String,
299    result: Result<String, String>,
300    box_id: u64,
301    path: &str,
302    destination: &str,
303    version: &str,
304) -> WebCodeOutput {
305    build(
306        tool_call_id,
307        vec![contents],
308        vec![Some(format!("File: {destination}"))],
309        result,
310        WEB_CODE_RENAME_FILE_METADATA_TYPE,
311        &RenameFileMetadata {
312            status: "renamed",
313            box_id,
314            path,
315            destination,
316            version,
317        },
318    )
319}
320
321pub fn delete_file(
322    tool_call_id: ToolCallId,
323    result: Result<String, String>,
324    box_id: u64,
325    path: &str,
326    version: &str,
327) -> WebCodeOutput {
328    build(
329        tool_call_id,
330        Vec::new(),
331        Vec::new(),
332        result,
333        WEB_CODE_DELETE_FILE_METADATA_TYPE,
334        &DeleteFileMetadata {
335            status: "deleted",
336            box_id,
337            path,
338            version,
339        },
340    )
341}
342
343pub fn check_success(
344    tool_call_id: ToolCallId,
345    messages: Vec<String>,
346    result: Result<String, String>,
347) -> WebCodeOutput {
348    build(
349        tool_call_id,
350        messages,
351        Vec::new(),
352        result,
353        WEB_CODE_CHECK_METADATA_TYPE,
354        &CheckMetadata {
355            status: "success",
356            fresh: true,
357            diagnostics: None,
358        },
359    )
360}
361
362pub fn check_failed(
363    tool_call_id: ToolCallId,
364    messages: Vec<String>,
365    result: Result<String, String>,
366    diagnostics: &str,
367) -> WebCodeOutput {
368    build(
369        tool_call_id,
370        messages,
371        Vec::new(),
372        result,
373        WEB_CODE_CHECK_METADATA_TYPE,
374        &CheckMetadata {
375            status: "failed",
376            fresh: true,
377            diagnostics: Some(diagnostics),
378        },
379    )
380}
381
382#[allow(clippy::too_many_arguments)]
383pub fn publish(
384    tool_call_id: ToolCallId,
385    messages: Vec<String>,
386    result: Result<String, String>,
387    version: &str,
388    source_object: &str,
389    winning: &str,
390    submitted: Option<&str>,
391    status: PublishStatus,
392) -> WebCodeOutput {
393    build(
394        tool_call_id,
395        messages,
396        Vec::new(),
397        result,
398        WEB_CODE_PUBLISH_METADATA_TYPE,
399        &PublishMetadata {
400            status: status.as_str(),
401            version,
402            source_object,
403            winning,
404            submitted,
405            check: "fresh",
406        },
407    )
408}
409
410pub fn error(
411    tool_call_id: ToolCallId,
412    messages: Vec<String>,
413    result: Result<String, String>,
414    operation: Operation,
415    code: &str,
416    source_object: Option<&str>,
417) -> WebCodeOutput {
418    build(
419        tool_call_id,
420        messages,
421        Vec::new(),
422        result,
423        operation.metadata_type(),
424        &ErrorMetadata {
425            status: "error",
426            code,
427            source_object,
428        },
429    )
430}
431
432fn build(
433    tool_call_id: ToolCallId,
434    messages: Vec<String>,
435    mut message_titles: Vec<Option<String>>,
436    result: Result<String, String>,
437    metadata_type: &str,
438    metadata: &impl Serialize,
439) -> WebCodeOutput {
440    if message_titles.is_empty() {
441        message_titles.resize(messages.len(), None);
442    }
443    assert_eq!(
444        messages.len(),
445        message_titles.len(),
446        "every WebCode message must have one title slot"
447    );
448    WebCodeOutput {
449        tool_call_id,
450        messages,
451        message_titles,
452        result,
453        metadata_type: metadata_type.to_owned(),
454        metadata_contents: serde_json::to_string(metadata)
455            .expect("fixed WebCode metadata serializes"),
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    fn id() -> ToolCallId {
464        ToolCallId::new([1; 12], 7)
465    }
466
467    #[test]
468    fn full_source_has_exact_parallel_file_titles() {
469        let paths = vec!["Documentation.md".to_owned(), "src/a.odd".to_owned()];
470        let value = create(
471            id(),
472            vec!["docs".into(), "code".into()],
473            Ok("created".into()),
474            "alpha",
475            "010101010101010101010101",
476            Language::JavaScript,
477            &paths,
478        );
479        assert_eq!(
480            value.message_titles,
481            [
482                Some("File: Documentation.md".to_owned()),
483                Some("File: src/a.odd".to_owned())
484            ]
485        );
486        assert!(value.metadata_contents.contains("\"file_message_count\":2"));
487        assert!(!value.metadata_contents.contains("authority"));
488    }
489
490    #[test]
491    fn every_operation_has_its_exact_metadata_type() {
492        assert_eq!(
493            Operation::CreateFile.metadata_type(),
494            "k1.web-code-create-file/v2"
495        );
496        assert_eq!(
497            Operation::RenameFile.metadata_type(),
498            "k1.web-code-rename-file/v2"
499        );
500        assert_eq!(
501            Operation::DeleteFile.metadata_type(),
502            "k1.web-code-delete-file/v2"
503        );
504        let value = overwrite(
505            id(),
506            Ok("Overwritten.".into()),
507            42,
508            "nested/file.js",
509            "020202020202020202020202",
510        );
511        assert_eq!(
512            value.metadata_contents,
513            r#"{"status":"overwritten","box_id":42,"path":"nested/file.js","version":"020202020202020202020202"}"#
514        );
515    }
516
517    #[test]
518    fn check_and_publish_are_always_fresh() {
519        let checked = check_failed(id(), Vec::new(), Err("bad".into()), "all diagnostics");
520        assert_eq!(
521            checked.metadata_contents,
522            r#"{"status":"failed","fresh":true,"diagnostics":"all diagnostics"}"#
523        );
524        assert!(checked.message_titles.is_empty());
525        let published = publish(
526            id(),
527            Vec::new(),
528            Ok("Published 1.2.3.".into()),
529            "1.2.3",
530            "object",
531            "winner",
532            None,
533            PublishStatus::Published,
534        );
535        assert_eq!(
536            published.metadata_contents,
537            r#"{"status":"published","version":"1.2.3","source_object":"object","winning":"winner","submitted":null,"check":"fresh"}"#
538        );
539    }
540}