use a2a_protocol_types::error::{A2aError, A2aResult};
use a2a_protocol_types::Task;
pub const CREATE_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS task_artifact_appends (
task_id TEXT NOT NULL,
artifact INTEGER NOT NULL,
seq INTEGER NOT NULL,
part TEXT NOT NULL,
PRIMARY KEY (task_id, artifact, seq),
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
) WITHOUT ROWID";
pub(super) const SELECT_FOR_TASK_SQL: &str =
"SELECT artifact, seq, part FROM task_artifact_appends \
WHERE task_id = ?1 ORDER BY artifact, seq";
pub(super) const DELETE_FOR_TASK_SQL: &str = "DELETE FROM task_artifact_appends WHERE task_id = ?1";
pub(super) type Row = (i64, i64, String);
pub(super) fn splice(task: &mut Task, rows: Vec<Row>) -> A2aResult<()> {
if rows.is_empty() {
return Ok(());
}
let Some(artifacts) = task.artifacts.as_mut() else {
return Ok(());
};
for (artifact_index, seq, part_json) in rows {
let Ok(index) = usize::try_from(artifact_index) else {
continue;
};
let Ok(position) = usize::try_from(seq) else {
continue;
};
let Some(artifact) = artifacts.get_mut(index) else {
continue;
};
if position < artifact.parts.len() {
continue;
}
if position > artifact.parts.len() {
continue;
}
let part = serde_json::from_str(&part_json).map_err(|e| {
A2aError::internal(format!(
"failed to deserialize journalled artifact part: {e}"
))
})?;
artifact.parts.push(part);
}
Ok(())
}
pub(super) fn rows_for_append(
task: &Task,
index: usize,
count: usize,
) -> A2aResult<Option<Vec<Row>>> {
if count == 0 {
return Ok(None);
}
let Some(artifacts) = task.artifacts.as_ref() else {
return Ok(None);
};
let Some(artifact) = artifacts.get(index) else {
return Ok(None);
};
if artifact.parts.len() < count {
return Ok(None);
}
let first = artifact.parts.len() - count;
let Ok(artifact_index) = i64::try_from(index) else {
return Ok(None);
};
let mut rows = Vec::with_capacity(count);
for (offset, part) in artifact.parts[first..].iter().enumerate() {
let Ok(seq) = i64::try_from(first + offset) else {
return Ok(None);
};
let json = serde_json::to_string(part)
.map_err(|e| A2aError::internal(format!("failed to serialize artifact part: {e}")))?;
rows.push((artifact_index, seq, json));
}
Ok(Some(rows))
}
#[cfg(test)]
mod tests {
use super::*;
use a2a_protocol_types::message::PartContent;
use a2a_protocol_types::{Artifact, ContextId, Part, Task, TaskId, TaskState, TaskStatus};
fn bare_task() -> Task {
Task {
id: TaskId::new("t1"),
context_id: ContextId::new("c1"),
status: TaskStatus::new(TaskState::Working),
history: None,
artifacts: None,
metadata: None,
}
}
fn task_with_parts(texts: &[&str]) -> Task {
let mut task = bare_task();
task.artifacts = Some(vec![Artifact::new(
"a1",
texts.iter().map(|t| Part::text(*t)).collect::<Vec<_>>(),
)]);
task
}
fn part_texts(task: &Task) -> Vec<String> {
task.artifacts
.as_ref()
.expect("artifacts")
.first()
.expect("one artifact")
.parts
.iter()
.map(|p| match &p.content {
PartContent::Text(text) => text.clone(),
other => panic!("unexpected part content: {other:?}"),
})
.collect()
}
fn row(seq: i64, text: &str) -> Row {
(
0,
seq,
serde_json::to_string(&Part::text(text)).expect("serialize"),
)
}
#[test]
fn journalled_parts_land_after_the_documents_own() {
let mut task = task_with_parts(&["a", "b"]);
splice(&mut task, vec![row(2, "c"), row(3, "d")]).expect("splice");
assert_eq!(part_texts(&task), ["a", "b", "c", "d"]);
}
#[test]
fn a_part_already_in_the_document_is_not_appended_twice() {
let mut task = task_with_parts(&["a", "b", "c"]);
splice(&mut task, vec![row(1, "b"), row(2, "c"), row(3, "d")]).expect("splice");
assert_eq!(part_texts(&task), ["a", "b", "c", "d"]);
}
#[test]
fn a_gap_stops_the_splice_rather_than_shifting_parts() {
let mut task = task_with_parts(&["a"]);
splice(&mut task, vec![row(2, "c"), row(3, "d")]).expect("splice");
assert_eq!(
part_texts(&task),
["a"],
"no part may be placed at an index that is not its own"
);
}
#[test]
fn rows_for_an_artifact_the_task_does_not_have_are_dropped() {
let mut task = task_with_parts(&["a"]);
splice(
&mut task,
vec![(7, 0, serde_json::to_string(&Part::text("x")).unwrap())],
)
.expect("splice");
assert_eq!(part_texts(&task), ["a"]);
}
#[test]
fn a_task_with_no_artifacts_is_left_alone() {
let mut task = bare_task();
splice(&mut task, vec![row(0, "x")]).expect("splice");
assert!(task.artifacts.is_none());
}
#[test]
fn an_unreadable_journal_row_fails_the_read() {
let mut task = task_with_parts(&["a"]);
let err = splice(&mut task, vec![(0, 1, "not json".to_string())])
.expect_err("a part this build cannot read must not be skipped silently");
assert!(err.message.contains("journalled artifact part"));
}
#[test]
fn an_append_journals_the_tail_at_its_own_positions() {
let task = task_with_parts(&["a", "b", "c"]);
let rows = rows_for_append(&task, 0, 2)
.expect("build rows")
.expect("the delta describes this task");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].1, 1, "seq is the part's index, not a counter");
assert_eq!(rows[1].1, 2);
}
#[test]
fn a_delta_that_does_not_describe_the_task_refuses_to_journal() {
let task = task_with_parts(&["a", "b"]);
assert!(
rows_for_append(&task, 0, 0).expect("no error").is_none(),
"an empty append has nothing to record"
);
assert!(
rows_for_append(&task, 9, 1).expect("no error").is_none(),
"an artifact index the task does not have"
);
assert!(
rows_for_append(&task, 0, 5).expect("no error").is_none(),
"more parts claimed appended than the artifact holds"
);
assert!(
rows_for_append(&bare_task(), 0, 1)
.expect("no error")
.is_none(),
"no artifacts at all"
);
}
#[test]
fn what_is_journalled_is_what_comes_back() {
let full = task_with_parts(&["a", "b", "c", "d"]);
let rows = rows_for_append(&full, 0, 3)
.expect("build rows")
.expect("describes the task");
let mut stored = task_with_parts(&["a"]);
splice(&mut stored, rows).expect("splice");
assert_eq!(part_texts(&stored), part_texts(&full));
}
}