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}