DirectProject 引用解析文本随条目持久化,prompt 投影拆成 freeze 与纯折叠
- AgcResourceReference 增加 resolved_text:入队冻结时写入素材摘要 + UI 设计文档代码上下文,此后随条目持久化;缺省合法(旧历史照样解析),序列化时不写 None - wire.rs 新增 freeze_direct_codex_user_item(校验 + 算片段 + 写盘 + 冻结进 part + 判空),direct_codex_user_item_to_prompt 改为只读条目自身事实的纯折叠(零 IO、零校验、无失败出口) - wire 投影与 turn input 优先读 resolved_text,缺省才退回按当前 manifest 现算摘要;历史回读依旧不产生写副作用 - user_input.rs 入队侧改用 freeze,prompt 由冻结条目折叠得出 - 生成绑定 DirectCodexUserContentPart 增加可选 resolvedText
This commit is contained in:
@@ -8,5 +8,5 @@ pub(crate) use model::DirectCodexUserItem;
|
||||
pub(crate) use validation::validate_direct_codex_user_item;
|
||||
pub(crate) use wire::{
|
||||
direct_codex_user_item_to_codex_turn_input, direct_codex_user_item_to_prompt,
|
||||
direct_codex_user_item_to_response_item,
|
||||
direct_codex_user_item_to_response_item, freeze_direct_codex_user_item,
|
||||
};
|
||||
|
||||
@@ -33,7 +33,14 @@ pub(crate) enum DirectCodexUserContentPart {
|
||||
#[serde(rename = "input_text")]
|
||||
InputText { text: String },
|
||||
#[serde(rename = "agc_resource_reference")]
|
||||
AgcResourceReference { resource_id: String },
|
||||
AgcResourceReference {
|
||||
resource_id: String,
|
||||
/// 这个引用被宿主解析出来的文本:素材摘要,加上引用的 UI 设计文档展开的代码上下文。
|
||||
/// 入队冻结时写入一次,此后随条目持久化,prompt / 历史回读 / turn input 共用这一份。
|
||||
/// 缺省合法(旧历史没有它照样解析),缺省时才退回按当前 manifest 现算。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
resolved_text: Option<String>,
|
||||
},
|
||||
#[serde(rename = "agc_skill_reference")]
|
||||
AgcSkillReference { name: String },
|
||||
#[serde(rename = "agc_runtime_region_reference")]
|
||||
@@ -89,6 +96,7 @@ mod tests {
|
||||
role: DirectCodexUserRole::User,
|
||||
content: vec![DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id: "asset-hero".to_string(),
|
||||
resolved_text: None,
|
||||
}],
|
||||
id: "turn-1".to_string(),
|
||||
});
|
||||
@@ -122,6 +130,32 @@ mod tests {
|
||||
assert!(error.to_string().contains("unknown field"), "{error}");
|
||||
}
|
||||
|
||||
/// 旧历史里的资源引用没有 `resolvedText`:缺省必须合法,照样解析成 `None` 的引用。
|
||||
#[test]
|
||||
fn resource_reference_without_resolved_text_still_parses() {
|
||||
let item: DirectCodexUserItem = serde_json::from_value(json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "agc_resource_reference",
|
||||
"resourceId": "asset-hero"
|
||||
}],
|
||||
"id": "turn-1"
|
||||
}))
|
||||
.expect("legacy resource reference must keep parsing");
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
match &message.content[0] {
|
||||
DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id,
|
||||
resolved_text,
|
||||
} => {
|
||||
assert_eq!(resource_id, "asset-hero");
|
||||
assert_eq!(resolved_text, &None);
|
||||
}
|
||||
other => panic!("expected a resource reference, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_reference_serializes_with_only_the_stable_name() {
|
||||
let item: DirectCodexUserItem = serde_json::from_value(json!({
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) fn validate_direct_codex_user_item(
|
||||
for part in &message.content {
|
||||
match part {
|
||||
DirectCodexUserContentPart::InputText { .. } => {}
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id, .. } => {
|
||||
reference_count = reference_count.saturating_add(1);
|
||||
validate_resource_id_and_manifest(&manifest, resource_id)?;
|
||||
}
|
||||
@@ -202,6 +202,7 @@ mod tests {
|
||||
assert!(content_has_meaningful_input(&[
|
||||
DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id: "asset-hero".to_string(),
|
||||
resolved_text: None,
|
||||
},
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use super::model::{
|
||||
};
|
||||
use super::validation::validate_direct_codex_user_item;
|
||||
use crate::agent::{
|
||||
read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type,
|
||||
sanitize_attachment_name, GameCreationAppManifest,
|
||||
sanitize_attachment_local_path, sanitize_attachment_media_type, sanitize_attachment_name,
|
||||
GameCreationAppManifest,
|
||||
};
|
||||
use crate::ui_editor::persistence::{
|
||||
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
|
||||
@@ -14,6 +14,53 @@ use crate::ui_editor::persistence::{
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
/// 入队检查产出的**冻结条目**:校验 → 把每个引用 part 的解析文本写进它自己的
|
||||
/// [`DirectCodexUserContentPart::AgcResourceReference::resolved_text`] → 判空。
|
||||
///
|
||||
/// 这是这条消息**唯一**会算片段、会写盘(UI 设计文档代码导出)的地方,也是唯一的失败出口:
|
||||
/// 之后的 prompt 折叠([`direct_codex_user_item_to_prompt`])与放行都不再重算、不再读 manifest,
|
||||
/// 因此也没有失败可言。冻结结果随条目一路走到历史、事件与 turn input——它们读的都是同一份文本。
|
||||
pub(crate) fn freeze_direct_codex_user_item(
|
||||
root: &Path,
|
||||
item: &DirectCodexUserItem,
|
||||
) -> Result<DirectCodexUserItem, String> {
|
||||
let manifest = validate_direct_codex_user_item(root, item)?;
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
let mut content = Vec::with_capacity(message.content.len());
|
||||
for part in &message.content {
|
||||
match part {
|
||||
DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id,
|
||||
resolved_text,
|
||||
} => {
|
||||
// 客户端不产出这个字段;它只可能来自"已冻结的条目"(旧历史缺省)。缺省才现算。
|
||||
let resolved_text = match resolved_text {
|
||||
Some(text) => Some(text.clone()),
|
||||
None => Some(resolved_resource_reference_text(
|
||||
root,
|
||||
&manifest,
|
||||
resource_id,
|
||||
)?),
|
||||
};
|
||||
content.push(DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id: resource_id.clone(),
|
||||
resolved_text,
|
||||
});
|
||||
}
|
||||
other => content.push(other.clone()),
|
||||
}
|
||||
}
|
||||
let frozen = DirectCodexUserItem::Message(DirectCodexUserMessageItem {
|
||||
role: message.role.clone(),
|
||||
content,
|
||||
id: message.id.clone(),
|
||||
});
|
||||
if direct_codex_user_item_to_prompt(&frozen).trim().is_empty() {
|
||||
return Err("DirectProject user item 不能转换为空 prompt".to_string());
|
||||
}
|
||||
Ok(frozen)
|
||||
}
|
||||
|
||||
/// 将历史中的 canonical user item 投影为 Codex `response_item` message。
|
||||
/// 非 user message 的标准 Response item 原样返回;未知形状直接失败。
|
||||
pub(crate) fn direct_codex_user_item_to_response_item(
|
||||
@@ -134,9 +181,15 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
||||
for part in &message.content {
|
||||
let text = match part {
|
||||
DirectCodexUserContentPart::InputText { text } => text.clone(),
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
resource_reference_summary(&manifest, resource_id)?
|
||||
}
|
||||
DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id,
|
||||
resolved_text,
|
||||
} => match resolved_text {
|
||||
// 冻结过的条目读自己那一份:历史回放因此就是当初发出去的那条消息。
|
||||
Some(text) => text.clone(),
|
||||
// 旧历史没有这份文本,退回按当前 manifest 现算(只有摘要,不产生写副作用)。
|
||||
None => resource_reference_summary(&manifest, resource_id)?,
|
||||
},
|
||||
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||
format!("${}", name.trim())
|
||||
}
|
||||
@@ -165,10 +218,16 @@ pub(crate) fn direct_codex_user_item_to_codex_turn_input(
|
||||
DirectCodexUserContentPart::InputText { text } => {
|
||||
input.push(serde_json::json!({ "type": "text", "text": text }));
|
||||
}
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
DirectCodexUserContentPart::AgcResourceReference {
|
||||
resource_id,
|
||||
resolved_text,
|
||||
} => {
|
||||
input.push(serde_json::json!({
|
||||
"type": "text",
|
||||
"text": resource_reference_summary(&manifest, resource_id)?,
|
||||
"text": match resolved_text {
|
||||
Some(text) => text.clone(),
|
||||
None => resource_reference_summary(&manifest, resource_id)?,
|
||||
},
|
||||
}));
|
||||
}
|
||||
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||
@@ -201,82 +260,88 @@ pub(crate) fn direct_codex_user_item_to_codex_turn_input(
|
||||
Ok(Value::Array(input))
|
||||
}
|
||||
|
||||
pub(crate) fn direct_codex_user_item_to_prompt(
|
||||
root: &Path,
|
||||
item: &DirectCodexUserItem,
|
||||
) -> Result<String, String> {
|
||||
let wire = direct_codex_user_item_to_wire_input(root, item)?;
|
||||
/// canonical 条目到 prompt 的**纯折叠**:零 IO、零校验、无失败出口。
|
||||
///
|
||||
/// 它只读条目自己的事实——文本 part 的正文与引用 part 的 `resolved_text`(入队时
|
||||
/// [`freeze_direct_codex_user_item`] 冻结,随条目持久化)。条目里没有引用之外的东西要算,
|
||||
/// 所以这里也不再需要 root。调用方要保证条目是冻结过的;缺省 `resolved_text` 的旧条目
|
||||
/// 走不了这一条(历史回读用 [`direct_codex_user_item_to_response_item`])。
|
||||
pub(crate) fn direct_codex_user_item_to_prompt(item: &DirectCodexUserItem) -> String {
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
let mut prompt = wire
|
||||
.as_array()
|
||||
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())?
|
||||
.iter()
|
||||
.filter_map(|part| part.get("text").and_then(Value::as_str))
|
||||
.collect::<String>();
|
||||
if let Some(code_context) = render_ui_design_code_context(root, message)? {
|
||||
prompt.push('\n');
|
||||
prompt.push_str(&code_context);
|
||||
let mut prompt = String::new();
|
||||
for part in &message.content {
|
||||
match part {
|
||||
DirectCodexUserContentPart::InputText { text } => prompt.push_str(text),
|
||||
DirectCodexUserContentPart::AgcResourceReference { resolved_text, .. } => {
|
||||
if let Some(text) = resolved_text {
|
||||
prompt.push_str(text);
|
||||
}
|
||||
}
|
||||
DirectCodexUserContentPart::AgcSkillReference { name } => {
|
||||
prompt.push_str(&format!("${}", name.trim()));
|
||||
}
|
||||
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
|
||||
prompt.push_str(&runtime_region_summary(reference));
|
||||
}
|
||||
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
|
||||
prompt.push_str(&attachment_reference_summary(reference));
|
||||
}
|
||||
}
|
||||
}
|
||||
if prompt.trim().is_empty() {
|
||||
return Err("DirectProject user item 不能转换为空 prompt".to_string());
|
||||
}
|
||||
Ok(prompt)
|
||||
prompt
|
||||
}
|
||||
|
||||
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
|
||||
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
|
||||
/// 一个资源引用 part 的解析文本:素材摘要,加上引用的 UI 设计文档展开的代码上下文。
|
||||
///
|
||||
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
|
||||
/// 的纯投影,不得在这里产生项目写副作用。
|
||||
fn render_ui_design_code_context(
|
||||
/// 只在 [`freeze_direct_codex_user_item`] 里现算一次;算出来的就是这一 part 此后唯一的文本。
|
||||
fn resolved_resource_reference_text(
|
||||
root: &Path,
|
||||
message: &DirectCodexUserMessageItem,
|
||||
) -> Result<Option<String>, String> {
|
||||
let referenced_ids = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
Some(resource_id.trim())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if referenced_ids.is_empty() {
|
||||
return Ok(None);
|
||||
manifest: &GameCreationAppManifest,
|
||||
resource_id: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut text = resource_reference_summary(manifest, resource_id)?;
|
||||
if let Some(code_context) = ui_design_code_context_for(root, manifest, resource_id) {
|
||||
text.push('\n');
|
||||
text.push_str(&code_context);
|
||||
}
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let mut lines = Vec::new();
|
||||
for resource_id in referenced_ids {
|
||||
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
|
||||
asset.id == resource_id
|
||||
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
});
|
||||
if !is_ui_design_doc {
|
||||
continue;
|
||||
}
|
||||
lines.push(
|
||||
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
asset_id: resource_id.to_string(),
|
||||
}) {
|
||||
Ok(result) => format!(
|
||||
prompt_text!("projectContext.uiDesign.codeContext"),
|
||||
result.relative_path
|
||||
),
|
||||
Err(error) => format!(
|
||||
prompt_text!("projectContext.uiDesign.generationErrorContext"),
|
||||
error = error
|
||||
),
|
||||
},
|
||||
);
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
/// 引用的 UI 设计文档上下文:复用 UI Editor 代码导出,把带文档注释的 JS 片段路径交给模型;
|
||||
/// 生成失败只带回原始错误,不阻断本轮引用。非 UI 设计文档返回 `None`。
|
||||
///
|
||||
/// 这是整条链上唯一会写盘(生成 `ui/generated-*.js`)的投影处,因此只允许在入队冻结时调用。
|
||||
/// 历史回读走 `direct_codex_user_item_to_response_item` 的纯投影,不得在这里产生项目写副作用。
|
||||
fn ui_design_code_context_for(
|
||||
root: &Path,
|
||||
manifest: &GameCreationAppManifest,
|
||||
resource_id: &str,
|
||||
) -> Option<String> {
|
||||
let resource_id = resource_id.trim();
|
||||
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
|
||||
asset.id == resource_id
|
||||
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
});
|
||||
if !is_ui_design_doc {
|
||||
return None;
|
||||
}
|
||||
if lines.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(lines.join("\n")))
|
||||
Some(
|
||||
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
asset_id: resource_id.to_string(),
|
||||
}) {
|
||||
Ok(result) => format!(
|
||||
prompt_text!("projectContext.uiDesign.codeContext"),
|
||||
result.relative_path
|
||||
),
|
||||
Err(error) => format!(
|
||||
prompt_text!("projectContext.uiDesign.generationErrorContext"),
|
||||
error = error
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -284,7 +349,7 @@ mod tests {
|
||||
use super::{
|
||||
direct_codex_user_item_to_codex_turn_input, direct_codex_user_item_to_prompt,
|
||||
direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input,
|
||||
validate_direct_codex_user_item,
|
||||
freeze_direct_codex_user_item, validate_direct_codex_user_item,
|
||||
};
|
||||
use crate::agent::direct_codex_user_item::model::DirectCodexUserItem;
|
||||
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
|
||||
@@ -375,8 +440,8 @@ mod tests {
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
let frozen = freeze_direct_codex_user_item(project.path(), &item).expect("freeze");
|
||||
let prompt = direct_codex_user_item_to_prompt(&frozen);
|
||||
assert!(
|
||||
prompt.contains(&format!(
|
||||
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
|
||||
@@ -387,6 +452,14 @@ mod tests {
|
||||
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
|
||||
"{prompt}"
|
||||
);
|
||||
// 解析文本随条目持久化:历史回放读的就是这一份,不再重算、不再写盘。
|
||||
let super::DirectCodexUserItem::Message(message) = &frozen;
|
||||
match &message.content[0] {
|
||||
super::DirectCodexUserContentPart::AgcResourceReference { resolved_text, .. } => {
|
||||
assert_eq!(resolved_text.as_deref(), Some(prompt.as_str()));
|
||||
}
|
||||
other => panic!("expected a frozen resource reference, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -418,8 +491,8 @@ mod tests {
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
let frozen = freeze_direct_codex_user_item(project.path(), &item).expect("freeze");
|
||||
let prompt = direct_codex_user_item_to_prompt(&frozen);
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
@@ -444,8 +517,8 @@ mod tests {
|
||||
],
|
||||
}))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
let frozen = freeze_direct_codex_user_item(project.path(), &item).expect("freeze");
|
||||
let prompt = direct_codex_user_item_to_prompt(&frozen);
|
||||
|
||||
let ui_doc_at = prompt
|
||||
.find(&failing_ui_doc_id)
|
||||
@@ -477,8 +550,8 @@ mod tests {
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
let frozen = freeze_direct_codex_user_item(project.path(), &item).expect("freeze");
|
||||
let prompt = direct_codex_user_item_to_prompt(&frozen);
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(
|
||||
!prompt.contains("请先阅读生成的带有文档的代码片段"),
|
||||
@@ -518,6 +591,42 @@ mod tests {
|
||||
assert_ne!(projected["content"][0]["type"], "text");
|
||||
}
|
||||
|
||||
/// 历史回读读条目自己的 `resolvedText`:引用被冻结过就是当初那条消息,没有就退回当前 manifest 的摘要。
|
||||
#[test]
|
||||
fn history_projection_reads_the_persisted_resolved_text() {
|
||||
let root = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
root.path(),
|
||||
"assets/hero.png",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
);
|
||||
for resolved_text in [None, Some("冻结时的解析文本".to_string())] {
|
||||
let mut reference = json!({
|
||||
"type": "agc_resource_reference",
|
||||
"resourceId": asset_id,
|
||||
});
|
||||
if let Some(text) = &resolved_text {
|
||||
reference["resolvedText"] = json!(text);
|
||||
}
|
||||
let item = json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"id": "turn-1:user",
|
||||
"content": [reference],
|
||||
});
|
||||
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
|
||||
.expect("history projection");
|
||||
let text = projected["content"][0]["text"].as_str().expect("text");
|
||||
assert_eq!(
|
||||
text,
|
||||
resolved_text.unwrap_or_else(|| format!(
|
||||
"[素材引用 resourceId={asset_id};项目路径=assets/hero.png]"
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_projection_preserves_empty_parts_line_breaks_and_trailing_whitespace() {
|
||||
let root = tempfile::tempdir().expect("temp project");
|
||||
@@ -541,7 +650,7 @@ mod tests {
|
||||
assert_eq!(projected["content"], item["content"]);
|
||||
let canonical = serde_json::from_value(item).expect("canonical user item");
|
||||
assert_eq!(
|
||||
direct_codex_user_item_to_prompt(root.path(), &canonical).expect("multiline prompt"),
|
||||
direct_codex_user_item_to_prompt(&canonical),
|
||||
"你好\n第二段\n "
|
||||
);
|
||||
}
|
||||
@@ -603,11 +712,9 @@ mod tests {
|
||||
"type": "message", "role": "user", "id": "turn-1:user", "content": content
|
||||
});
|
||||
let canonical = serde_json::from_value(item.clone()).expect("canonical user item");
|
||||
assert_eq!(
|
||||
direct_codex_user_item_to_prompt(root.path(), &canonical)
|
||||
.expect("reference prompt"),
|
||||
expected_prompt
|
||||
);
|
||||
let frozen = freeze_direct_codex_user_item(root.path(), &canonical)
|
||||
.expect("reference freeze");
|
||||
assert_eq!(direct_codex_user_item_to_prompt(&frozen), expected_prompt);
|
||||
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
|
||||
.expect("reference history projection");
|
||||
assert_eq!(
|
||||
|
||||
@@ -30,8 +30,8 @@ pub(crate) fn normalize_direct_client_turn_id(
|
||||
|
||||
/// DirectProject 聊天命令:**只入队**,不 await、也不起回合。
|
||||
///
|
||||
/// 入队侧就是"这条消息能不能收下":`clientTurnId` 校验 → 工作流恢复 → 用户条目校验 → prompt
|
||||
/// 投影 → 前置条件 → 容量预判 → 工程准备,任何一步失败都是**入队失败**(typed 载荷)。全部通过
|
||||
/// 入队侧就是"这条消息能不能收下":`clientTurnId` 校验 → 工作流恢复 → 用户条目校验与冻结
|
||||
/// (引用解析文本写进条目)→ 前置条件 → 容量预判 → 工程准备,任何一步失败都是**入队失败**(typed 载荷)。全部通过
|
||||
/// 之后只做一件事——把待发消息排进队列:不登记占用、不落盘、不发回合事件、不起 codex。
|
||||
///
|
||||
/// 于是"这一轮跑成什么"仍然只有订阅事件一个来源:命令返回 `Ok` 只说明**入队成立**。真正的回合边界
|
||||
@@ -65,7 +65,7 @@ pub(crate) async fn enqueue_direct_codex_turn(
|
||||
}
|
||||
|
||||
/// 入队侧:全程 typed,顺序固定,**每一步失败都还是入队失败**:
|
||||
/// `clientTurnId` 校验 → 工作流恢复 → 用户条目校验 → prompt 投影 → 前置条件 → 容量预判 → 工程准备 → 入队。
|
||||
/// `clientTurnId` 校验 → 工作流恢复 → 用户条目校验与冻结 → 前置条件 → 容量预判 → 工程准备 → 入队。
|
||||
///
|
||||
/// 这个顺序不是风格问题:工程准备(可能建工程、铺脚手架)必须在"这条消息真的被收下"之前判定;
|
||||
/// 没通过检查的东西不进队列。容量放在工程准备之前是"别为注定收不下的消息先付分钟级的代价",它
|
||||
@@ -88,10 +88,10 @@ async fn enqueue_direct_codex_turn_typed(
|
||||
),
|
||||
}
|
||||
})?;
|
||||
validate_direct_codex_user_item(root, &user_item)
|
||||
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
|
||||
let user_prompt = direct_codex_user_item_to_prompt(root, &user_item)
|
||||
// 冻结是这条消息唯一的算片段 / 写盘时机:引用解析文本就此写进条目自身,放行只重投影。
|
||||
let user_item = freeze_direct_codex_user_item(root, &user_item)
|
||||
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
|
||||
let user_prompt = direct_codex_user_item_to_prompt(&user_item);
|
||||
check_direct_turn_preconditions(root, &user_prompt, creation_type.as_deref())?;
|
||||
let thread_id = thread_id_for_project(root);
|
||||
// 容量是**最便宜**的一条入队前置条件,先判:满了就直接说满,别让一条注定收不下的消息先去做工程
|
||||
|
||||
+10
-1
@@ -4,7 +4,16 @@ import type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeR
|
||||
|
||||
export type DirectCodexUserContentPart =
|
||||
| { type: 'input_text'; text: string }
|
||||
| { type: 'agc_resource_reference'; resourceId: string }
|
||||
| {
|
||||
type: 'agc_resource_reference';
|
||||
resourceId: string;
|
||||
/**
|
||||
* 这个引用被宿主解析出来的文本:素材摘要,加上引用的 UI 设计文档展开的代码上下文。
|
||||
* 入队冻结时写入一次,此后随条目持久化,prompt / 历史回读 / turn input 共用这一份。
|
||||
* 缺省合法(旧历史没有它照样解析),缺省时才退回按当前 manifest 现算。
|
||||
*/
|
||||
resolvedText?: string | null;
|
||||
}
|
||||
| { type: 'agc_skill_reference'; name: string }
|
||||
| ({
|
||||
type: 'agc_runtime_region_reference';
|
||||
|
||||
Reference in New Issue
Block a user