blob: fde9fa3d70b0501bf4d2d7a22afb232821e40040 [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 super::Inventory;
use crate::StoreError;
use maka_agent_facts::artifact::Artifact;
use maka_agent_facts::model::ModelUsage;
use maka_agent_facts::pricing::Quote;
use maka_agent_facts::session::CopyRequest;
use maka_agent_facts::session::CopyState;
use maka_agent_facts::session::Lineage;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tokio::io::{AsyncWrite, AsyncWriteExt};
pub(super) const MAGIC: &[u8] = b"MAKA-SESSION\0\x02";
pub(super) const CHUNK: usize = 64 * 1024;
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub(super) enum Record {
Header {
inventory: Inventory,
source_high_water: u64,
},
Session {
id: String,
parent: Option<String>,
created_at: u64,
updated_at: u64,
archived: bool,
configuration: serde_json::Value,
},
Copy(Copy),
Member {
session: String,
sequence: u64,
archive_sequence: Option<u64>,
},
RevisionSource {
session: String,
sequence: u64,
},
HistoryArtifact {
session: String,
source_session: String,
source_artifact: String,
artifact: String,
},
/// Preserve source bytes, including ordering, rather than serialize a decoded event again.
Event {
sequence: u64,
json: String,
},
Blob(Blob),
Accounting {
event_id: String,
quote: Quote,
valuation: Option<Valuation>,
},
End {
digest: String,
frames: u64,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Copy {
pub request: CopyRequest,
pub through: u64,
pub observed_through: u64,
pub lineage: Lineage,
pub state: CopyState,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Valuation {
pub usage: ModelUsage,
pub usd: Option<f64>,
}
/// Exactly the declared raw bytes follow this frame; binary data is not JSON/base64.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "resource", rename_all = "snake_case", deny_unknown_fields)]
pub(super) enum Blob {
ToolResult {
event_id: String,
bytes: u64,
digest: String,
},
Artifact {
metadata: Artifact,
digest: String,
/// Selected owner-history consumption witness, not the source SQL stamp.
frozen_at: Option<u64>,
},
Composition {
event_id: String,
bytes: u64,
digest: String,
},
}
impl Blob {
pub fn bytes(&self) -> u64 {
match self {
Self::ToolResult { bytes, .. } | Self::Composition { bytes, .. } => *bytes,
Self::Artifact { metadata, .. } => metadata.size_bytes,
}
}
pub fn digest(&self) -> &str {
match self {
Self::ToolResult { digest, .. }
| Self::Artifact { digest, .. }
| Self::Composition { digest, .. } => digest,
}
}
}
pub(super) struct Writer<W> {
output: W,
digest: Sha256,
bytes: u64,
frames: u64,
}
impl<W: AsyncWrite + Unpin> Writer<W> {
pub async fn new(output: W) -> Result<Self, StoreError> {
let mut writer = Self {
output,
digest: Sha256::new(),
bytes: 0,
frames: 0,
};
writer.bytes(MAGIC).await?;
Ok(writer)
}
pub async fn record(&mut self, record: &Record) -> Result<(), StoreError> {
let bytes = serde_json::to_vec(record)?;
let length = u32::try_from(bytes.len()).map_err(|_| {
StoreError::InvalidTransition("bundle frame length is not representable".into())
})?;
self.bytes(&length.to_be_bytes()).await?;
self.bytes(&bytes).await?;
self.frames = self
.frames
.checked_add(1)
.ok_or(StoreError::PrefixTooLarge)?;
Ok(())
}
pub async fn bytes(&mut self, bytes: &[u8]) -> Result<(), StoreError> {
write_progress(&mut self.output, bytes).await?;
self.digest.update(bytes);
self.bytes = self
.bytes
.checked_add(bytes.len() as u64)
.ok_or(StoreError::PrefixTooLarge)?;
Ok(())
}
pub async fn finish(mut self) -> Result<(W, u64, String), StoreError> {
let digest = format!("sha256:{}", hex::encode(self.digest.clone().finalize()));
self.record(&Record::End {
digest: digest.clone(),
frames: self.frames,
})
.await?;
tokio::time::timeout(std::time::Duration::from_secs(30), self.output.flush())
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "bundle flush stalled")
})??;
Ok((self.output, self.bytes, digest))
}
}
/// Bound stalled I/O while allowing a complete frame/blob to keep progressing.
pub(super) async fn write_progress<W: AsyncWrite + Unpin>(
output: &mut W,
mut bytes: &[u8],
) -> Result<(), StoreError> {
while !bytes.is_empty() {
let written = tokio::time::timeout(std::time::Duration::from_secs(30), output.write(bytes))
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "bundle destination stalled")
})??;
if written == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"bundle destination stopped accepting bytes",
)
.into());
}
bytes = &bytes[written..];
}
Ok(())
}