use serde_json::{Map, Value};
pub type Metadata = Map<String, Value>;
mod attachments;
mod cancellation;
mod ids;
mod subagent;
mod trace;
mod xml;
pub use attachments::RunAttachments;
pub use cancellation::CancellationToken;
pub use ids::{AgentId, CheckpointId, ConversationId, RunId, SessionId, TaskId};
pub use subagent::{SubagentLifecycleEvent, SubagentLifecycleKind, SubagentSpec};
pub use trace::TraceContext;
pub use xml::{XmlWriter, escape_xml_attribute, escape_xml_text};
pub const SDK_NAME: &str = "starweaver-agent-sdk";
#[must_use]
pub const fn sdk_name() -> &'static str {
SDK_NAME
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
#[test]
fn exposes_sdk_name() {
assert_eq!(sdk_name(), "starweaver-agent-sdk");
}
#[test]
fn creates_prefixed_ids() {
assert_eq!(AgentId::default().as_str(), "main");
assert_eq!(AgentId::from_string("agent-1").as_str(), "agent-1");
assert_eq!(
SessionId::from_string("session-fixed").as_str(),
"session-fixed"
);
assert!(RunId::new().as_str().starts_with("run_"));
assert_eq!(RunId::from_string("run-fixed").as_str(), "run-fixed");
assert!(ConversationId::new().as_str().starts_with("conv_"));
assert_eq!(
ConversationId::from_string("conv-fixed").as_str(),
"conv-fixed"
);
assert!(CheckpointId::new().as_str().starts_with("ckpt_"));
assert_eq!(
CheckpointId::from_string("ckpt-fixed").as_str(),
"ckpt-fixed"
);
assert!(TaskId::new().as_str().starts_with("task_"));
assert_eq!(TaskId::from_string("task-fixed").as_str(), "task-fixed");
}
#[test]
fn run_attachments_wrap_metadata() {
let mut attachments = RunAttachments::new();
assert!(attachments.is_empty());
attachments.insert("tenant", Value::String("alpha".to_string()));
assert_eq!(
attachments.get("tenant"),
Some(&Value::String("alpha".to_string()))
);
assert_eq!(attachments.len(), 1);
let metadata: Metadata = attachments.clone().into();
assert_eq!(metadata["tenant"], "alpha");
assert_eq!(RunAttachments::from(metadata).values["tenant"], "alpha");
}
#[test]
fn parses_and_builds_trace_context() {
let context = TraceContext::from_trace_parent(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
)
.with_span_id("span-1")
.with_trace_state("vendor=state");
assert_eq!(
context.trace_id.as_deref(),
Some("4bf92f3577b34da6a3ce929d0e0e4736")
);
assert_eq!(context.parent_span_id.as_deref(), Some("00f067aa0ba902b7"));
assert_eq!(context.span_id.as_deref(), Some("span-1"));
assert_eq!(context.trace_state.as_deref(), Some("vendor=state"));
assert_eq!(context.metadata["trace_flags"], "01");
assert!(!context.is_empty());
let mut metadata = Metadata::default();
metadata.insert("tenant".to_string(), Value::String("acme".to_string()));
let fallback = TraceContext::from_trace_parent("trace-id")
.with_parent_span_id("parent")
.with_metadata(metadata.clone());
assert_eq!(fallback.trace_id.as_deref(), Some("trace-id"));
assert_eq!(fallback.parent_span_id.as_deref(), Some("parent"));
assert_eq!(fallback.metadata, metadata);
assert!(TraceContext::new().is_empty());
}
#[test]
fn trace_parent_parser_ignores_invalid_w3c_shapes() {
for invalid in [
"00-short-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-xyz067aa0ba902b7-01",
"00-00000000000000000000000000000000-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
"ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra",
] {
let context = TraceContext::from_trace_parent(invalid);
assert_eq!(context.trace_id.as_deref(), Some(invalid));
assert_eq!(context.parent_span_id, None);
assert!(context.metadata.is_empty());
}
}
#[test]
fn builds_subagent_specs_lifecycle_events() {
let spec = SubagentSpec::new("research", "Research helper", "Find facts")
.with_tools(vec!["search".to_string()])
.with_optional_tools(vec!["browser".to_string()]);
assert_eq!(spec.name, "research");
assert_eq!(spec.description, "Research helper");
assert_eq!(spec.system_prompt, "Find facts");
assert_eq!(spec.tools, ["search"]);
assert_eq!(spec.optional_tools, ["browser"]);
let event = SubagentLifecycleEvent::new(
SubagentLifecycleKind::Completed,
"research",
TaskId::from_string("task-1"),
)
.with_run_id(RunId::from_string("run-1"))
.with_metadata(serde_json::json!({"ok": true}));
assert_eq!(event.kind, SubagentLifecycleKind::Completed);
assert_eq!(event.name, "research");
assert_eq!(event.task_id.as_str(), "task-1");
assert_eq!(event.run_id.as_ref().map(RunId::as_str), Some("run-1"));
assert_eq!(event.metadata["ok"], true);
}
}