This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -0,0 +1,15 @@
[package]
name = "module-creative-agent"
edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
shared-kernel = { workspace = true }
spacetimedb = { workspace = true, optional = true }

View File

@@ -0,0 +1,225 @@
use shared_kernel::{normalize_optional_string, normalize_required_string};
use crate::{
CreativeAgentError, CreativeAgentMessageAppendInput, CreativeAgentMessageKind,
CreativeAgentMessageRole, CreativeAgentStage, CreativeAgentStageUpdateInput,
CreativeAgentTargetBindInput, CreativeAgentTemplateConfirmInput, CreativeTargetPlayType,
};
pub fn validate_create_session(
session_id: &str,
owner_user_id: &str,
) -> Result<(String, String), CreativeAgentError> {
let session_id =
normalize_required_string(session_id).ok_or(CreativeAgentError::MissingSessionId)?;
let owner_user_id =
normalize_required_string(owner_user_id).ok_or(CreativeAgentError::MissingOwnerUserId)?;
Ok((session_id, owner_user_id))
}
pub fn validate_append_message(
input: &CreativeAgentMessageAppendInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if normalize_required_string(&input.message_id).is_none() {
return Err(CreativeAgentError::MissingMessageId);
}
if normalize_required_string(&input.text).is_none() {
return Err(CreativeAgentError::MissingMessageText);
}
Ok(())
}
pub fn validate_stage_update(
current: CreativeAgentStage,
input: &CreativeAgentStageUpdateInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
validate_stage_transition(current, input.stage)
}
pub fn validate_template_confirmation(
current: CreativeAgentStage,
input: &CreativeAgentTemplateConfirmInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if normalize_required_string(&input.template_selection_json).is_none() {
return Err(CreativeAgentError::MissingTemplateSelection);
}
if !input.template_selection_json.contains("\"costRange\"")
&& !input.template_selection_json.contains("\"cost_range\"")
{
return Err(CreativeAgentError::MissingCostRange);
}
validate_stage_transition(current, CreativeAgentStage::PlanningPuzzleLevels)
}
pub fn validate_target_binding(
current_stage: CreativeAgentStage,
template_selection_json: Option<&str>,
input: &CreativeAgentTargetBindInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if input.play_type != CreativeTargetPlayType::Puzzle {
return Err(CreativeAgentError::UnsupportedTargetPlayType);
}
if normalize_required_string(&input.target_session_id).is_none() {
return Err(CreativeAgentError::MissingTargetSessionId);
}
if normalize_optional_string(template_selection_json.map(str::to_string)).is_none() {
return Err(CreativeAgentError::TemplateNotConfirmed);
}
// 中文注释:绑定目标 session 是“草稿已创建”的持久化标记,只允许在行动链路之后发生。
if !matches!(
current_stage,
CreativeAgentStage::PlanningPuzzleLevels
| CreativeAgentStage::Acting
| CreativeAgentStage::Reflecting
| CreativeAgentStage::Collaborating
| CreativeAgentStage::TargetReady
) {
return Err(CreativeAgentError::InvalidStageTransition);
}
Ok(())
}
pub fn validate_stage_transition(
current: CreativeAgentStage,
next: CreativeAgentStage,
) -> Result<(), CreativeAgentError> {
if current == next {
return Ok(());
}
if matches!(
next,
CreativeAgentStage::Failed | CreativeAgentStage::WaitingUser
) {
return Ok(());
}
let allowed = matches!(
(current, next),
(CreativeAgentStage::Idle, CreativeAgentStage::Perceiving)
| (
CreativeAgentStage::Idle,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (CreativeAgentStage::Perceiving, CreativeAgentStage::Thinking)
| (
CreativeAgentStage::Thinking,
CreativeAgentStage::Remembering
)
| (
CreativeAgentStage::Thinking,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (
CreativeAgentStage::Remembering,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (
CreativeAgentStage::SelectingPuzzleTemplate,
CreativeAgentStage::WaitingTemplateConfirmation
)
| (
CreativeAgentStage::WaitingTemplateConfirmation,
CreativeAgentStage::PlanningPuzzleLevels
)
| (
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::Acting
)
| (CreativeAgentStage::Acting, CreativeAgentStage::Reflecting)
| (
CreativeAgentStage::Reflecting,
CreativeAgentStage::Collaborating
)
| (
CreativeAgentStage::Collaborating,
CreativeAgentStage::Acting
)
| (
CreativeAgentStage::Reflecting,
CreativeAgentStage::TargetReady
)
| (CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
| (
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::TargetReady
)
);
if allowed {
Ok(())
} else {
Err(CreativeAgentError::InvalidStageTransition)
}
}
pub fn normalize_message_role(value: CreativeAgentMessageRole) -> &'static str {
value.as_str()
}
pub fn normalize_message_kind(value: CreativeAgentMessageKind) -> &'static str {
value.as_str()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CreativeAgentTargetBindInput, CreativeTargetStage};
#[test]
fn template_confirmation_requires_cost_range() {
let input = CreativeAgentTemplateConfirmInput {
session_id: "creative-session-1".to_string(),
owner_user_id: "user-1".to_string(),
template_selection_json: r#"{"templateId":"puzzle.default-creative"}"#.to_string(),
updated_at_micros: 1,
};
assert_eq!(
validate_template_confirmation(CreativeAgentStage::WaitingTemplateConfirmation, &input,),
Err(CreativeAgentError::MissingCostRange)
);
}
#[test]
fn target_binding_requires_confirmed_template() {
let input = CreativeAgentTargetBindInput {
binding_id: "creative-binding-1".to_string(),
session_id: "creative-session-1".to_string(),
owner_user_id: "user-1".to_string(),
play_type: CreativeTargetPlayType::Puzzle,
target_session_id: "puzzle-session-1".to_string(),
target_stage: CreativeTargetStage::PuzzleResult,
result_profile_id: None,
created_at_micros: 1,
};
assert_eq!(
validate_target_binding(CreativeAgentStage::Acting, None, &input),
Err(CreativeAgentError::TemplateNotConfirmed)
);
}
#[test]
fn phase1_stage_path_allows_template_to_target_ready() {
assert!(
validate_stage_transition(
CreativeAgentStage::WaitingTemplateConfirmation,
CreativeAgentStage::PlanningPuzzleLevels,
)
.is_ok()
);
assert!(
validate_stage_transition(
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::Acting
)
.is_ok()
);
assert!(
validate_stage_transition(CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
.is_ok()
);
}
}

View File

@@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
use crate::{
CreativeAgentMessageKind, CreativeAgentMessageRole, CreativeAgentStage,
CreativeInputSummarySnapshot, CreativeTargetPlayType, CreativeTargetStage,
};
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionCreateInput {
pub session_id: String,
pub owner_user_id: String,
pub input_summary: CreativeInputSummarySnapshot,
pub welcome_message_id: Option<String>,
pub welcome_message_text: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionGetInput {
pub session_id: String,
pub owner_user_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentMessageAppendInput {
pub session_id: String,
pub owner_user_id: String,
pub message_id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text: String,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentStageUpdateInput {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTemplateConfirmInput {
pub session_id: String,
pub owner_user_id: String,
pub template_selection_json: String,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentLevelPlanSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub level_plan_json: String,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTargetBindInput {
pub binding_id: String,
pub session_id: String,
pub owner_user_id: String,
pub play_type: CreativeTargetPlayType,
pub target_session_id: String,
pub target_stage: CreativeTargetStage,
pub result_profile_id: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentFinalizeInput {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub assistant_message_id: Option<String>,
pub assistant_message_text: Option<String>,
pub updated_at_micros: i64,
}

View File

@@ -0,0 +1,187 @@
//! 创意互动 Agent 领域模型。
//!
//! 本 crate 只描述会话、阶段、消息和目标绑定的纯领域事实LLM、SSE、
//! 图片生成和 SpacetimeDB 写表均留在外层 adapter。
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
pub const CREATIVE_AGENT_SESSION_ID_PREFIX: &str = "creative-session-";
pub const CREATIVE_AGENT_MESSAGE_ID_PREFIX: &str = "creative-message-";
pub const CREATIVE_AGENT_BINDING_ID_PREFIX: &str = "creative-binding-";
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentStage {
Idle,
Perceiving,
Thinking,
Remembering,
SelectingPuzzleTemplate,
WaitingTemplateConfirmation,
PlanningPuzzleLevels,
Acting,
Reflecting,
Collaborating,
TargetReady,
WaitingUser,
Failed,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentMessageRole {
User,
Assistant,
System,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentMessageKind {
Chat,
Stage,
ActionResult,
Warning,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeTargetPlayType {
Puzzle,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeTargetStage {
PuzzleAgentWorkspace,
PuzzleResult,
PuzzleRuntime,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeInputImageSnapshot {
pub asset_id: Option<String>,
pub read_url: Option<String>,
pub thumbnail_url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub summary: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeInputSummarySnapshot {
pub text: Option<String>,
pub entry_context: String,
pub images: Vec<CreativeInputImageSnapshot>,
pub material_summary: Option<String>,
pub unsupported_capabilities_json: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentMessageSnapshot {
pub message_id: String,
pub session_id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text: String,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTargetBindingSnapshot {
pub binding_id: String,
pub session_id: String,
pub play_type: CreativeTargetPlayType,
pub target_session_id: String,
pub target_stage: CreativeTargetStage,
pub result_profile_id: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionSnapshot {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub input_summary: CreativeInputSummarySnapshot,
pub messages: Vec<CreativeAgentMessageSnapshot>,
pub puzzle_template_selection_json: Option<String>,
pub puzzle_image_generation_plan_json: Option<String>,
pub target_binding: Option<CreativeAgentTargetBindingSnapshot>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionProcedureResult {
pub ok: bool,
pub session_json: Option<String>,
pub error_message: Option<String>,
}
impl CreativeAgentStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Perceiving => "perceiving",
Self::Thinking => "thinking",
Self::Remembering => "remembering",
Self::SelectingPuzzleTemplate => "selecting_puzzle_template",
Self::WaitingTemplateConfirmation => "waiting_template_confirmation",
Self::PlanningPuzzleLevels => "planning_puzzle_levels",
Self::Acting => "acting",
Self::Reflecting => "reflecting",
Self::Collaborating => "collaborating",
Self::TargetReady => "target_ready",
Self::WaitingUser => "waiting_user",
Self::Failed => "failed",
}
}
}
impl CreativeAgentMessageRole {
pub fn as_str(self) -> &'static str {
match self {
Self::User => "user",
Self::Assistant => "assistant",
Self::System => "system",
}
}
}
impl CreativeAgentMessageKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Chat => "chat",
Self::Stage => "stage",
Self::ActionResult => "action_result",
Self::Warning => "warning",
}
}
}
impl CreativeTargetPlayType {
pub fn as_str(self) -> &'static str {
match self {
Self::Puzzle => "puzzle",
}
}
}
impl CreativeTargetStage {
pub fn as_str(self) -> &'static str {
match self {
Self::PuzzleAgentWorkspace => "puzzle-agent-workspace",
Self::PuzzleResult => "puzzle-result",
Self::PuzzleRuntime => "puzzle-runtime",
}
}
}

View File

@@ -0,0 +1,35 @@
use std::{error::Error, fmt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CreativeAgentError {
MissingSessionId,
MissingOwnerUserId,
MissingMessageId,
MissingMessageText,
MissingTemplateSelection,
MissingCostRange,
MissingTargetSessionId,
InvalidStageTransition,
TemplateNotConfirmed,
UnsupportedTargetPlayType,
}
impl fmt::Display for CreativeAgentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::MissingSessionId => "creative session_id 缺失",
Self::MissingOwnerUserId => "creative owner_user_id 缺失",
Self::MissingMessageId => "creative message_id 缺失",
Self::MissingMessageText => "creative message text 缺失",
Self::MissingTemplateSelection => "拼图模板选择缺失",
Self::MissingCostRange => "拼图模板积分范围缺失",
Self::MissingTargetSessionId => "目标拼图 session 缺失",
Self::InvalidStageTransition => "创意 Agent 阶段迁移不合法",
Self::TemplateNotConfirmed => "拼图模板未确认,不能创建草稿",
Self::UnsupportedTargetPlayType => "Phase 1 只允许绑定拼图 target",
};
write!(f, "{message}")
}
}
impl Error for CreativeAgentError {}

View File

@@ -0,0 +1,9 @@
mod application;
mod commands;
mod domain;
mod errors;
pub use application::*;
pub use commands::*;
pub use domain::*;
pub use errors::*;