blob: 083bea7807f7ac01cceed2eacc88ddfe69fc9669 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::import::Record;
use maka_agent_facts::import::Source;
use maka_capabilities::filesystem::ReadError;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Storage(#[from] maka_capabilities::storage::StoreError),
#[error(transparent)]
Remote(#[from] maka_agent_ports::remote::Error),
#[error(transparent)]
Read(#[from] ReadError),
#[error(transparent)]
Database(#[from] maka_capabilities::filesystem::database::Error),
#[error("invalid source record at line {line}: {source}")]
Decode {
line: u64,
source: serde_json::Error,
},
#[error("source limit exceeded: {kind} (maximum {max})")]
Limit { kind: &'static str, max: u64 },
#[error("invalid source: {0}")]
Invalid(&'static str),
}
/// Evidence for one fixed prefix, including any incomplete final write. Pinning
/// alone does not freeze in-place writes; multi-pass formats compare this digest.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Fingerprint {
pub bytes: u64,
pub sha256: String,
pub incomplete_tail: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transcript {
pub source: Source,
/// Source claims only; never a workspace or model authorization.
pub cwd: Option<String>,
pub title: String,
pub records: Vec<Record>,
pub fingerprint: Fingerprint,
}
#[derive(Default)]
pub(crate) struct Records {
records: Vec<Record>,
}
impl Records {
pub fn push(&mut self, record: Record) -> Result<(), Error> {
record.validate().map_err(Error::Invalid)?;
self.records.push(record);
Ok(())
}
pub fn finish(self) -> Result<Vec<Record>, Error> {
if !self.records.iter().any(Record::is_conversation) {
return Err(Error::Invalid("source contains no readable conversation"));
}
Ok(self.records)
}
pub fn retain(&mut self, keep: impl FnMut(&Record) -> bool) -> Result<(), Error> {
self.records.retain(keep);
Ok(())
}
}
pub(crate) fn identity(value: &str) -> Result<(), Error> {
if value.is_empty() || value.chars().any(char::is_control) {
Err(Error::Invalid("invalid source identity"))
} else {
Ok(())
}
}
pub(crate) fn source_cwd(value: Option<String>) -> Option<String> {
value.filter(|path| !path.is_empty() && !path.chars().any(char::is_control))
}
pub(crate) fn title(text: &str) -> String {
let mut output = String::new();
let mut remaining = 160;
for word in text.split_whitespace() {
if !output.is_empty() {
if remaining <= 1 {
break;
}
output.push(' ');
remaining -= 1;
}
for character in word.chars().take(remaining) {
output.push(character);
remaining -= 1;
}
if remaining == 0 {
break;
}
}
output
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum Timestamp {
Number(f64),
Text(String),
}
impl Timestamp {
pub fn millis(&self) -> Option<u64> {
self.convert(false)
}
pub fn codex_millis(&self) -> Option<u64> {
self.convert(true)
}
fn convert(&self, codex: bool) -> Option<u64> {
let number = match self {
Self::Number(value) => Some(*value),
Self::Text(value) => value.parse().ok(),
};
let millis = match number {
Some(value) if value.is_finite() && value >= 0.0 => {
if codex && value < 1_000_000_000_000.0 {
value * 1000.0
} else {
value
}
}
Some(_) => return None,
None => {
let Self::Text(value) = self else { return None };
value.parse::<jiff::Timestamp>().ok()?.as_millisecond() as f64
}
};
(0.0..=9_007_199_254_740_991.0)
.contains(&millis)
.then_some(millis as u64)
}
}